Compare commits

...

122 Commits

Author SHA1 Message Date
Anthony Lapenna a171e540c5 Merge branch 'release/1.20.2' 2019-03-05 17:34:28 +13:00
Anthony Lapenna cb858f0412 chore(version): bump version number 2019-03-05 17:34:19 +13:00
Anthony Lapenna 82078a8d8f style(extensions): update extensions information panel 2019-03-05 16:09:03 +13:00
Anthony Lapenna 2b31f489d9 feat(api): add support for an externally fetched title for motd (#2755)
* feat(api): add support for an externally fetched title for motd

* refactor(api): gofmt motd.go

* refactor(api): update go comment
2019-03-05 16:05:15 +13:00
Anthony Lapenna e2a17480af Merge branch 'develop' of github.com:portainer/portainer into develop 2019-03-04 13:48:01 +13:00
Anthony Lapenna 0670079566 feat(api): update ExtensionDefinitionsURL 2019-03-04 13:46:27 +13:00
Anthony Lapenna 5ca9501540 dep(api): update docker binary version to 18.09.3 (#2749) 2019-03-01 14:45:36 +13:00
Anthony Lapenna 415c1759d1 Merge branch 'oath-poc' into develop 2019-03-01 14:16:04 +13:00
Anthony Lapenna db0091b46d feat(api): revert extension URLs to correct one 2019-03-01 13:58:55 +13:00
linquize 42529cc5ea feat(volumes): show volume creation date (#2745) 2019-03-01 11:59:11 +13:00
Anthony Lapenna 60fbfeba23 fix(oauth): fix settings displaying issue for custom OAuth configuration 2019-03-01 11:24:47 +13:00
Anthony Lapenna f5091ce5fb fix(auth): fix invalid condition to display OAuth login button 2019-03-01 10:58:18 +13:00
Anthony Lapenna 58962de20e Merge branch 'develop' into oath-poc 2019-03-01 09:42:38 +13:00
Anthony Lapenna 1eb7e6bacc fix(auth): rollback changes introduced via #2591 (#2747) 2019-02-28 11:38:02 +13:00
Anthony Lapenna 130baddea0 fix(api): fix an issue when removing non local administrators 2019-02-25 18:54:21 +13:00
Tim van den Eijnden 9cbf1f34a7 feat(networks): prevent removal of predefined networks (#2697)
* fix(networks): disable removing predefined networks (#1838)

*  fix(networks): disable select all for predefined networks (#1838)

* fix(networks): do not allow delete in network-details & use constant (#1838)
2019-02-25 14:25:48 +13:00
linquize c152d3f62e fix(stacks): update web editor to set tab key to insert spaces (#2735) 2019-02-25 14:19:53 +13:00
linquize da44f14e07 fix(auth): prevent redirect parameter to use state portainer.auth (#2701) 2019-02-25 13:57:11 +13:00
Anthony Lapenna 49516e2c3f style(oauth): update Azure UI elements 2019-02-25 13:38:27 +13:00
Anthony Lapenna 9c4c782a90 style(container-creation): review auto remove element position 2019-02-25 13:09:09 +13:00
baron_l 7aa6a30614 feat(registry-manager): allow regular users to use the registry browse feature (#2664)
* feat(registries): registries accessibility to all authorized people and not only admins

* feat(registry): dockerhub settings for admin only

* feat(registry): remove registry config access for non admin users

* feat(api): use AuthenticatedAccess policy instead of RestrictedAccess for extensionList operation

* refactor(api): minor update to security package

* refactor(api): revert unexporting function changes

* refactor(api): apply gofmt
2019-02-25 13:02:49 +13:00
linquize 99e50370bd feat(container-creation): support auto remove option (docker run --rm) (#2684) 2019-02-25 09:48:31 +13:00
Anthony Lapenna dc2a8cf1f4 feat(oauth): update OAuth configuration UX 2019-02-21 14:02:25 +13:00
Anthony Lapenna b9ac3d4286 feat(oauth): fix the double refresh issue 2019-02-21 11:09:57 +13:00
Anthony Lapenna 6711e6c969 feat(oauth): update configuration override UX 2019-02-21 10:30:09 +13:00
Anthony Lapenna 4a5fa211a7 feat(account): display a warning message in the account view 2019-02-20 13:57:13 +13:00
Anthony Lapenna d510d23408 feat(oauth): improve Azure OAuth support 2019-02-20 13:53:25 +13:00
Anthony Lapenna ce9e009e22 feat(oauth): update UI/UX 2019-02-19 14:38:42 +13:00
Anthony Lapenna 9918c1260b feat(oauth): update authentication panel with OAuth provider details 2019-02-19 09:54:02 +13:00
Anthony Lapenna e325ad10dd fix(oauth): fix an UX issue when updating microsoft oauth settings 2019-02-18 16:18:48 +13:00
Anthony Lapenna 73f20b5157 refactor(oauth): remove console log statement 2019-02-18 15:21:34 +13:00
Anthony Lapenna b6f04c5e0d fix(oauth): fix missing scopes for microsoft provider 2019-02-18 15:21:06 +13:00
Anthony Lapenna 2ef8c0b33e fix(app): rewrite URLHelper to avoid an issue with minification 2019-02-18 15:08:54 +13:00
Anthony Lapenna 7643f8d08c feat(oauth): dev build supporting Oauth extension 2019-02-18 14:46:34 +13:00
Anthony Lapenna 086bad2956 Merge branch 'develop' into oath-poc 2019-02-18 09:58:51 +13:00
Anthony Lapenna d5dfc889bb docs(README): remove gitter badges 2019-02-18 09:51:20 +13:00
Montana Flynn ef926dce33 docs(README): update logo src (#2719)
The current logo src is 404: https://portainer.io/images/logo_alt.png

The repo already includes the logo: https://github.com/portainer/portainer/blob/develop/assets/images/logo_alt.png?raw=true
2019-02-18 09:49:34 +13:00
Anthony Lapenna d768e72a21 feat(oauth): add support for default team 2019-02-17 19:01:42 +13:00
Anthony Lapenna 78e2aaf7d4 feat(oauth): update OAuth UX 2019-02-17 17:01:36 +13:00
Anthony Lapenna 17cf374c30 Merge branch 'develop' into oath-poc 2019-02-17 16:39:23 +13:00
Nathan Baum 165096bef0 refactor(api): fix a typo (#2712)
Just a trivial spelling error.
2019-02-15 09:12:53 +13:00
Anthony Lapenna de76ba4e67 feat(oauth): update OAuth UX 2019-02-14 15:58:45 +13:00
linquize b1e048e218 feat(build-system): prefix some dependencies with "semver:" (#2690)
This makes both npm and yarn to work
2019-02-14 12:13:48 +13:00
linquize 8f32d58fae fix(templates): redirect to home if endpoint not yet selected #2709 (#2710) 2019-02-14 12:08:46 +13:00
Anthony Lapenna 16226b1202 Merge branch 'oath-poc' of github.com:portainer/pportainer into oath-poc 2019-02-13 10:01:06 +13:00
baron_l 8f568c8699 style(oauth): oauth loading + oauth config rework 2019-02-08 16:07:16 +01:00
Anthony Lapenna af34b99cd4 Merge branch 'develop' into oath-poc 2019-02-08 13:32:53 +13:00
baron_l 2755527d28 feat(oauth): default team for user on oauth settings 2019-02-07 19:32:02 +01:00
baron_l 4d8133f696 feat(oauth): spinner on code evaluation after sucessfull oauth 2019-02-07 15:07:10 +01:00
Anthony Lapenna fdc11dbe3a feat(build-system): update build system (#2682) 2019-02-07 12:00:47 +13:00
Anthony Lapenna 508352f4ea Merge branch 'develop' into oath-poc 2019-02-04 09:19:12 +13:00
Daniel Cardoza 9b6b6e09ae fix(endpoints): correct agent stack download url (#2667)
* 2584 fix(endpoints): correct agent stack download url

The directions for installing the agent stack from the endpoints
view used an old url. Update to the new url.

* Drop the portainer- prefix for the download path and filename

Co-Authored-By: dang3r <danielpcardoza@gmail.com>
2019-02-04 09:06:07 +13:00
Anthony Lapenna 899cd5f279 fix(home): fix an issue when trying to connect to an Azure ACI endpoint (#2671) 2019-02-04 09:04:52 +13:00
Anthony Lapenna 2eec8b75d0 Merge tag '1.20.1' into develop
Release 1.20.1
2019-01-31 13:15:28 +13:00
Anthony Lapenna 048c74a0dc Merge branch 'release/1.20.1' 2019-01-31 13:15:23 +13:00
Anthony Lapenna 6b1c476b63 chore(version): bump version number 2019-01-31 13:15:18 +13:00
Anthony Lapenna c5b5f80bea docs(README): update build badge 2019-01-31 12:02:12 +13:00
Anthony Lapenna cea2c60b55 refactor(build-system): fix lint issues 2019-01-31 11:38:27 +13:00
Steven Kang 576f369152 feat(build-system): introduce Azure DevOps support (#2666) 2019-01-31 11:37:16 +13:00
Anthony Lapenna fca4f619b5 fix(api): re-use previous password when ldap settings update use empty password (#2659) 2019-01-30 14:53:14 +13:00
Chaim Lev Ari 90281fd7f0 feat(oauth): add providers to providers-selector 2019-01-25 10:57:40 +02:00
Chaim Lev Ari c1939f6070 feature(oauth): add provider selector 2019-01-25 10:46:17 +02:00
Chaim Lev Ari 50c604ee4c fix(auth): use the right function to oauth validate 2019-01-25 10:44:31 +02:00
Chaim Lev Ari 41ded64037 Revert "refactor(auth): extract oauth login mechanism to service"
This reverts commit 0a439b3893.
2019-01-25 10:37:23 +02:00
baron_l 801336336f fix(registry-manager): add repositories pagination support (#2641)
* fix(registry-management): add support for repositories list with multiple requests

* refactor(registry-management): change regex usage to a reusable interceptor function

* refactor(registry-management): change interceptor to transformResponse function
2019-01-24 13:38:36 +13:00
Anthony Lapenna 90a0998502 feat(templates): add sonatype nexus 3 template 2019-01-23 16:05:07 +13:00
Anthony Lapenna 1a4dff536d fix(container-creation): fix an issue with command parsing (#2642)
* fix(container-creation): fix an issue with command parsing

* refactor(container-creation): remove indentation update
2019-01-23 12:25:42 +13:00
Chaim Lev-Ari f772cd31cb feat(auth): preserve url when redirected to login (#2591)
* feat(auth): preserve url when redirected to login

* feat(auth): add redirect also to unauthenticated flow

* style(app): remove style changes from files

* fix(app): remove reference to otpLogin

* style(auth): remove semicolon
2019-01-23 12:22:56 +13:00
Chaim Lev-Ari 8160fe4717 feat(app): redirect to home if no endpoint is set (#2601)
* refactor(stacks): set newstack state as a child state of stacks

* fix(docker): add check on docker states for endpoint

* refactor(app): remove redirect notification
2019-01-23 12:21:48 +13:00
Anthony Lapenna 86c60807cd feat(endpoint-creation): fix invalid link (#2644) 2019-01-23 12:18:18 +13:00
baron_l c1f2d90997 fix(container-creation): fix missing capabilities on duplicate (#2635) 2019-01-23 09:28:44 +13:00
Chaim Lev Ari 3699b794eb feat(oauth): add providers selectors 2019-01-18 12:14:12 +02:00
Chaim Lev Ari 69252a8377 refactour(auth): move information body to each setting 2019-01-18 12:08:18 +02:00
Chaim Lev Ari 193e7eb3f8 refactor(oauth): remove separation of strings 2019-01-18 11:53:44 +02:00
Chaim Lev Ari de5f6086d0 refactor(oauth): return parse content error 2019-01-18 11:51:41 +02:00
Chaim Lev Ari 46e8f10aea refactor(ouath): use oauth2 library to get token 2019-01-18 10:56:16 +02:00
Chaim Lev Ari 60040e90d0 refactor(oauth): move build url logic to service 2019-01-18 10:24:42 +02:00
Chaim Lev Ari c5c06b307a refactor(oauth): rename authenticate function 2019-01-18 10:15:02 +02:00
Chaim Lev Ari c28274667d refactor(oauth): use oauth2 to generate login url 2019-01-18 10:13:33 +02:00
Anthony Lapenna 54163e3b92 fix(extensions): fix an issue with extensions with expired licenses (#2628)
* fix(extensions): fix an issue with extensions with expired licenses

* fix(api): fix invalid log call

* fix(api): allow to re-enable an extension
2019-01-18 10:00:18 +13:00
Chaim Lev-Ari 62eb47b3cb fix(container-creation): revert container state if creation failed (#2565)
* fix(container): rename old container only if exist

* fix(container): remove new container only if created

* style(container): fix typo

Co-Authored-By: chiptus <chiptus@users.noreply.github.com>
2019-01-18 08:59:43 +13:00
Anthony Lapenna 808eb7d341 dep(bootstrap): update bootstrap version to 3.4.0 (#2632) 2019-01-18 08:51:12 +13:00
hiyao a33eca4bbb fix(registry-manager): fix an issue when removing all tags of a repository (#2545)
* fix repository reload got error in remove tags

When I remove all tags, removeTags() will reload and do initView() again, but data.tags response null, that trigger data.tags.length got error.

* Revert "fix repository reload got error in remove tags"

This reverts commit 5d9b1778ef91aefd7969909d60b68ca55cbcc705.

* fix(registry-management): change response repository tags type to array by force

* feat(registry-management): redirect to repositories page when no tag in the repository after delete tags
2019-01-18 08:01:47 +13:00
baron_l 50e77d2bf1 fix(network-details): displaying all subnets and gateways on network details (#2629) 2019-01-17 11:39:15 +13:00
DevHugo 50a3b08209 feat(app): add driver name in the volume selector for container/service creation (#2534)
* Feat(containers): add driver name in the volume selector

* Feat(services): add driver name in the volume selector
2019-01-17 11:28:40 +13:00
Chaim Lev Ari 0a439b3893 refactor(auth): extract oauth login mechanism to service 2019-01-16 18:57:15 +02:00
Chaim Lev Ari 0d4e1d00f0 refactor(login): move oauth button to right 2019-01-16 18:00:01 +02:00
Chaim Lev Ari b09f491f62 style(auth): remove comments and change error 2019-01-16 17:53:10 +02:00
Chaim Lev Ari dc067b3308 refactor(http): remove old oauth handler 2019-01-16 17:41:56 +02:00
Chaim Lev Ari b121f975fa refactor(settings): remove duplicate settings 2019-01-16 17:38:07 +02:00
Chaim Lev Ari 3f44925d7e fix(auth): fix typo - missing function 2019-01-16 17:37:50 +02:00
Chaim Lev Ari 80d570861d refactor(auth): move public settings into view model 2019-01-16 17:34:12 +02:00
Chaim Lev Ari 317bd53e43 Merge branch 'oath-poc' of github.com:portainer/pportainer into oath-poc 2019-01-16 17:26:29 +02:00
Chaim Lev Ari 24f066716b refactor(auth): expose only the login url 2019-01-16 17:25:16 +02:00
Chaim Lev Ari 4cbde7bb0d refactor(auth): move oauth handler under auth 2019-01-16 17:24:58 +02:00
Chaim Lev Ari f6bdc5c2b3 refactor(auth): move oauth handler code to its own file 2019-01-16 17:01:38 +02:00
Anthony Lapenna c650fe56c2 fix(auth): fix typos
Co-Authored-By: chiptus <chiptus@users.noreply.github.com>
2019-01-16 16:53:24 +02:00
Anthony Lapenna fc8938e871 fix(auth): change oauth error type
Co-Authored-By: chiptus <chiptus@users.noreply.github.com>
2019-01-16 16:50:19 +02:00
Anthony Lapenna 44b7e0fdca fix(auth): change error type
Co-Authored-By: chiptus <chiptus@users.noreply.github.com>
2019-01-16 16:49:33 +02:00
Chaim Lev-Ari fe63b4a156 fix(container-creation): populate logger config from existing container (#2602)
* refactor(container): change map function to lodash

* style(container): add semicolon
2019-01-16 13:34:28 +13:00
Chaim Lev-Ari 42365a52b1 feat(container-details): change network identifier to name (#2623) 2019-01-16 08:05:55 +13:00
Mark Stansberry d6aafceba8 docs(api): update swagger definitions 2019-01-16 08:04:47 +13:00
baron_l c7983d8993 fix(app): remove endpoint status update on 502/503 http return
* refactor(app): removing unused dep and function
2019-01-16 07:58:35 +13:00
Anthony Lapenna 34667bd3b3 fix(network-creation): force overlay network creation on manager node (#2622)
* fix(network-creation): force overlay network creation on manager node

* fix(app): fix function override

* fix(app): use portainerAgentManagerOperation in interceptor
2019-01-15 12:10:29 +13:00
Anthony Lapenna 3a3577754e fix(home): only display group name if available (#2621) 2019-01-15 08:52:26 +13:00
Anthony Lapenna bed49c37e4 fix(teams): remove name sanitization when creating a team (#2619) 2019-01-14 17:27:55 +13:00
Anthony Lapenna dedc02cc8d docs(api): fix invalid example value for AutoCreateUsers property (#2618) 2019-01-14 16:50:53 +13:00
Chaim Lev Ari 17ac3e5ed1 refactor(oauth): move enpoint constant to extension 2019-01-03 13:36:17 +02:00
Chaim Lev Ari 25620c5008 refactor(auth): refactor get url params 2019-01-02 20:49:25 +02:00
Chaim Lev Ari 9bebe9dee7 refactor(auth): move user setter into function 2019-01-02 20:01:23 +02:00
Chaim Lev Ari 81e3ace232 fix(auth): fix oauh enabled function 2019-01-02 20:01:06 +02:00
Chaim Lev Ari 15b6941872 refactor(oauth): move oauth rest service to extension 2019-01-02 20:00:41 +02:00
Chaim Lev Ari 7aaa9e58e9 refactor(auth): move oauth info to component 2019-01-02 16:24:10 +02:00
Chaim Lev Ari 515daf6dba refactor(auth): exprt oauth settings into extension 2019-01-02 16:21:36 +02:00
Chaim Lev Ari 0a1643bbcf style(auth): remove added spaces 2019-01-02 16:01:10 +02:00
Chaim Lev Ari 38f24683a6 refactor(auth): remove empty $q.deffered 2019-01-02 15:59:38 +02:00
Chaim Lev Ari 7494101a4d refactor(auth): refactor auth controller 2019-01-02 15:56:08 +02:00
Chaim Lev Ari 996319d299 feat(auth): don't clear client secret on update 2018-12-30 18:39:16 +02:00
Chaim Lev Ari 2ee6f2780b refactor(oauth): add debug logs 2018-12-30 18:25:30 +02:00
Chaim Lev Ari 241a701eca feat(oauth): merge pr from https://github.com/portainer/portainer/pull/2515 2018-12-30 18:02:22 +02:00
Anthony Lapenna 463b379876 docs(README): remove broken badges and links 2018-12-27 09:03:13 +01:00
Chaim Lev-Ari f2cd33e831 feat(container-creation): call stopAndRename after pullImage (#2564)
* refactor(container): remove bind of function
2018-12-21 00:37:35 +09:00
105 changed files with 1568 additions and 482 deletions
+2 -7
View File
@@ -1,15 +1,13 @@
<p align="center">
<img title="portainer" src='https://portainer.io/images/logo_alt.png' />
<img title="portainer" src='https://github.com/portainer/portainer/blob/develop/assets/images/logo_alt.png?raw=true' />
</p>
[![Docker Pulls](https://img.shields.io/docker/pulls/portainer/portainer.svg)](https://hub.docker.com/r/portainer/portainer/)
[![Microbadger](https://images.microbadger.com/badges/image/portainer/portainer.svg)](http://microbadger.com/images/portainer/portainer "Image size")
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable)
[![Build Status](https://semaphoreci.com/api/v1/portainer/portainer-ci/branches/develop/badge.svg)](https://semaphoreci.com/portainer/portainer-ci)
[![Build Status](https://portainer.visualstudio.com/Portainer%20CI/_apis/build/status/Portainer%20CI?branchName=develop)](https://portainer.visualstudio.com/Portainer%20CI/_build/latest?definitionId=3&branchName=develop)
[![Code Climate](https://codeclimate.com/github/portainer/portainer/badges/gpa.svg)](https://codeclimate.com/github/portainer/portainer)
[![Slack](https://portainer.io/slack/badge.svg)](https://portainer.io/slack/)
[![Gitter](https://badges.gitter.im/portainer/Lobby.svg)](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6)
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
@@ -20,8 +18,6 @@
## Demo
<img src="https://portainer.io/images/screenshots/portainer.gif" width="77%"/>
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
Please note that the public demo cluster is **reset every 15min**.
@@ -44,7 +40,6 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart
* Issues: https://github.com/portainer/portainer/issues
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
* Slack (chat): https://portainer.io/slack/
* Gitter (chat): https://gitter.im/portainer/Lobby
## Reporting bugs and contributing
+1
View File
@@ -139,6 +139,7 @@ func (store *Store) MigrateData() error {
DatabaseVersion: version,
EndpointGroupService: store.EndpointGroupService,
EndpointService: store.EndpointService,
ExtensionService: store.ExtensionService,
ResourceControlService: store.ResourceControlService,
SettingsService: store.SettingsService,
StackService: store.StackService,
+19
View File
@@ -0,0 +1,19 @@
package migrator
func (m *Migrator) updateExtensionsToDBVersion17() error {
legacyExtensions, err := m.extensionService.Extensions()
if err != nil {
return err
}
for _, extension := range legacyExtensions {
extension.License.Valid = true
err = m.extensionService.Persist(&extension)
if err != nil {
return err
}
}
return nil
}
+12
View File
@@ -5,6 +5,7 @@ import (
"github.com/portainer/portainer"
"github.com/portainer/portainer/bolt/endpoint"
"github.com/portainer/portainer/bolt/endpointgroup"
"github.com/portainer/portainer/bolt/extension"
"github.com/portainer/portainer/bolt/resourcecontrol"
"github.com/portainer/portainer/bolt/settings"
"github.com/portainer/portainer/bolt/stack"
@@ -20,6 +21,7 @@ type (
db *bolt.DB
endpointGroupService *endpointgroup.Service
endpointService *endpoint.Service
extensionService *extension.Service
resourceControlService *resourcecontrol.Service
settingsService *settings.Service
stackService *stack.Service
@@ -35,6 +37,7 @@ type (
DatabaseVersion int
EndpointGroupService *endpointgroup.Service
EndpointService *endpoint.Service
ExtensionService *extension.Service
ResourceControlService *resourcecontrol.Service
SettingsService *settings.Service
StackService *stack.Service
@@ -52,6 +55,7 @@ func NewMigrator(parameters *Parameters) *Migrator {
currentDBVersion: parameters.DatabaseVersion,
endpointGroupService: parameters.EndpointGroupService,
endpointService: parameters.EndpointService,
extensionService: parameters.ExtensionService,
resourceControlService: parameters.ResourceControlService,
settingsService: parameters.SettingsService,
templateService: parameters.TemplateService,
@@ -210,5 +214,13 @@ func (m *Migrator) Migrate() error {
}
}
// Portainer 1.20.1
if m.currentDBVersion < 17 {
err := m.updateExtensionsToDBVersion17()
if err != nil {
return err
}
}
return m.versionService.StoreDBVersion(portainer.DBVersion)
}
+8 -4
View File
@@ -175,7 +175,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
endpointSyncJob := &portainer.EndpointSyncJob{}
endointSyncSchedule := &portainer.Schedule{
endpointSyncSchedule := &portainer.Schedule{
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
Name: "system_endpointsync",
CronExpression: "@every " + *flags.SyncInterval,
@@ -186,14 +186,14 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
}
endpointSyncJobContext := cron.NewEndpointSyncJobContext(endpointService, *flags.ExternalEndpoints)
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endointSyncSchedule, endpointSyncJobContext)
endpointSyncJobRunner := cron.NewEndpointSyncJobRunner(endpointSyncSchedule, endpointSyncJobContext)
err = jobScheduler.ScheduleJob(endpointSyncJobRunner)
if err != nil {
return err
}
return scheduleService.CreateSchedule(endointSyncSchedule)
return scheduleService.CreateSchedule(endpointSyncSchedule)
}
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error {
@@ -260,6 +260,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
portainer.LDAPGroupSearchSettings{},
},
},
OAuthSettings: portainer.OAuthSettings{},
AllowBindMountsForRegularUsers: true,
AllowPrivilegedModeForRegularUsers: true,
EnableHostManagementFeatures: false,
@@ -486,7 +487,10 @@ func initExtensionManager(fileService portainer.FileService, extensionService po
for _, extension := range extensions {
err := extensionManager.EnableExtension(&extension, extension.License.LicenseKey)
if err != nil {
return nil, err
log.Printf("Unable to enable extension: %s [extension: %s]", err.Error(), extension.Name)
extension.Enabled = false
extension.License.Valid = false
extensionService.Persist(&extension)
}
}
+3 -1
View File
@@ -18,7 +18,8 @@ import (
var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/extensions/"
var extensionBinaryMap = map[portainer.ExtensionID]string{
portainer.RegistryManagementExtension: "extension-registry-management",
portainer.RegistryManagementExtension: "extension-registry-management",
portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
}
// ExtensionManager represents a service used to
@@ -113,6 +114,7 @@ func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension,
LicenseKey: licenseKey,
Company: licenseDetails[0],
Expiration: licenseDetails[1],
Valid: true,
}
extension.Version = licenseDetails[2]
+138
View File
@@ -0,0 +1,138 @@
package auth
import (
"encoding/json"
"io/ioutil"
"net/http"
"log"
"github.com/asaskevich/govalidator"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/portainer"
)
type oauthPayload struct {
Code string
}
func (payload *oauthPayload) Validate(r *http.Request) error {
if govalidator.IsNull(payload.Code) {
return portainer.Error("Invalid OAuth authorization code")
}
return nil
}
func (handler *Handler) authenticateThroughExtension(code, licenseKey string, settings *portainer.OAuthSettings) (string, error) {
extensionURL := handler.ProxyManager.GetExtensionURL(portainer.OAuthAuthenticationExtension)
encodedConfiguration, err := json.Marshal(settings)
if err != nil {
return "", nil
}
req, err := http.NewRequest("GET", extensionURL+"/validate", nil)
if err != nil {
return "", err
}
client := &http.Client{}
req.Header.Set("X-OAuth-Config", string(encodedConfiguration))
req.Header.Set("X-OAuth-Code", code)
req.Header.Set("X-PortainerExtension-License", licenseKey)
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
type extensionResponse struct {
Username string `json:"Username,omitempty"`
Err string `json:"err,omitempty"`
Details string `json:"details,omitempty"`
}
var extResp extensionResponse
err = json.Unmarshal(body, &extResp)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", portainer.Error(extResp.Err + ":" + extResp.Details)
}
return extResp.Username, nil
}
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
var payload oauthPayload
err := request.DecodeAndValidateJSONPayload(r, &payload)
if err != nil {
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
}
settings, err := handler.SettingsService.Settings()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
}
if settings.AuthenticationMethod != 3 {
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", portainer.Error("OAuth authentication is not enabled")}
}
extension, err := handler.ExtensionService.Extension(portainer.OAuthAuthenticationExtension)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err}
} else if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
}
username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings)
if err != nil {
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", portainer.ErrUnauthorized}
}
user, err := handler.UserService.UserByUsername(username)
if err != nil && err != portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a user with the specified username from the database", err}
}
if user == nil && !settings.OAuthSettings.OAuthAutoCreateUsers {
return &httperror.HandlerError{http.StatusForbidden, "Account not created beforehand in Portainer and automatic user provisioning not enabled", portainer.ErrUnauthorized}
}
if user == nil {
user = &portainer.User{
Username: username,
Role: portainer.StandardUserRole,
}
err = handler.UserService.CreateUser(user)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist user inside the database", err}
}
if settings.OAuthSettings.DefaultTeamID != 0 {
membership := &portainer.TeamMembership{
UserID: user.ID,
TeamID: settings.OAuthSettings.DefaultTeamID,
Role: portainer.TeamMember,
}
err = handler.TeamMembershipService.CreateTeamMembership(membership)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
}
}
}
return handler.writeToken(w, user)
}
+6
View File
@@ -6,6 +6,7 @@ import (
"github.com/gorilla/mux"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/portainer"
"github.com/portainer/portainer/http/proxy"
"github.com/portainer/portainer/http/security"
)
@@ -28,6 +29,8 @@ type Handler struct {
SettingsService portainer.SettingsService
TeamService portainer.TeamService
TeamMembershipService portainer.TeamMembershipService
ExtensionService portainer.ExtensionService
ProxyManager *proxy.Manager
}
// NewHandler creates a handler to manage authentication operations.
@@ -36,6 +39,9 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi
Router: mux.NewRouter(),
authDisabled: authDisabled,
}
h.Handle("/auth/oauth/validate",
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.validateOAuth)))).Methods(http.MethodPost)
h.Handle("/auth",
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
@@ -42,7 +42,7 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request)
}
for _, existingExtension := range extensions {
if existingExtension.ID == extensionID {
if existingExtension.ID == extensionID && existingExtension.Enabled {
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", portainer.ErrExtensionAlreadyEnabled}
}
}
@@ -42,6 +42,7 @@ func associateExtensionData(definition *portainer.Extension, extensions []portai
definition.Enabled = extension.Enabled
definition.License.Company = extension.License.Company
definition.License.Expiration = extension.License.Expiration
definition.License.Valid = extension.License.Valid
definitionVersion := semver.New(definition.Version)
extensionVersion := semver.New(extension.Version)
+1 -1
View File
@@ -23,7 +23,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
}
h.Handle("/extensions",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
h.Handle("/extensions",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
h.Handle("/extensions/{id}",
+8 -1
View File
@@ -10,6 +10,7 @@ import (
)
type motdResponse struct {
Title string `json:"Title"`
Message string `json:"Message"`
Hash []byte `json:"Hash"`
}
@@ -22,6 +23,12 @@ func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
return
}
title, err := client.Get(portainer.MessageOfTheDayTitleURL, 0)
if err != nil {
response.JSON(w, &motdResponse{Message: ""})
return
}
hash := crypto.HashFromBytes(motd)
response.JSON(w, &motdResponse{Message: string(motd), Hash: hash})
response.JSON(w, &motdResponse{Title: string(title), Message: string(motd), Hash: hash})
}
+5 -3
View File
@@ -18,6 +18,7 @@ func hideFields(registry *portainer.Registry) {
// Handler is the HTTP handler used to handle registry operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
RegistryService portainer.RegistryService
ExtensionService portainer.ExtensionService
FileService portainer.FileService
@@ -27,7 +28,8 @@ type Handler struct {
// NewHandler creates a handler to manage registry operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
Router: mux.NewRouter(),
requestBouncer: bouncer,
}
h.Handle("/registries",
@@ -35,7 +37,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/registries",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet)
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut)
h.Handle("/registries/{id}/access",
@@ -45,7 +47,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
h.Handle("/registries/{id}",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
h.PathPrefix("/registries/{id}/v2").Handler(
bouncer.AdministratorAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
return h
}
+5
View File
@@ -24,6 +24,11 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
err = handler.requestBouncer.RegistryAccess(r, registry)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
}
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
if err == portainer.ErrObjectNotFound {
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
@@ -23,6 +23,11 @@ func (handler *Handler) registryInspect(w http.ResponseWriter, r *http.Request)
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
}
err = handler.requestBouncer.RegistryAccess(r, registry)
if err != nil {
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", portainer.ErrEndpointAccessDenied}
}
hideFields(registry)
return response.JSON(w, registry)
}
+1
View File
@@ -11,6 +11,7 @@ import (
func hideFields(settings *portainer.Settings) {
settings.LDAPSettings.Password = ""
settings.OAuthSettings.ClientSecret = ""
}
// Handler is the HTTP handler used to handle settings operations.
@@ -1,6 +1,7 @@
package settings
import (
"fmt"
"net/http"
httperror "github.com/portainer/libhttp/error"
@@ -15,6 +16,7 @@ type publicSettingsResponse struct {
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
ExternalTemplates bool `json:"ExternalTemplates"`
OAuthLoginURI string `json:"OAuthLoginURI"`
}
// GET request on /api/settings/public
@@ -31,6 +33,11 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
ExternalTemplates: false,
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
settings.OAuthSettings.AuthorizationURI,
settings.OAuthSettings.ClientID,
settings.OAuthSettings.RedirectURI,
settings.OAuthSettings.Scopes),
}
if settings.TemplatesURL != "" {
+17 -2
View File
@@ -16,6 +16,7 @@ type settingsUpdatePayload struct {
BlackListedLabels []portainer.Pair
AuthenticationMethod *int
LDAPSettings *portainer.LDAPSettings
OAuthSettings *portainer.OAuthSettings
AllowBindMountsForRegularUsers *bool
AllowPrivilegedModeForRegularUsers *bool
EnableHostManagementFeatures *bool
@@ -24,8 +25,8 @@ type settingsUpdatePayload struct {
}
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 {
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal) or 2 (LDAP/AD)")
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 {
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)")
}
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
return portainer.Error("Invalid logo URL. Must correspond to a valid URL format")
@@ -66,7 +67,21 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
}
if payload.LDAPSettings != nil {
ldapPassword := settings.LDAPSettings.Password
if payload.LDAPSettings.Password != "" {
ldapPassword = payload.LDAPSettings.Password
}
settings.LDAPSettings = *payload.LDAPSettings
settings.LDAPSettings.Password = ldapPassword
}
if payload.OAuthSettings != nil {
clientSecret := payload.OAuthSettings.ClientSecret
if clientSecret == "" {
clientSecret = settings.OAuthSettings.ClientSecret
}
settings.OAuthSettings = *payload.OAuthSettings
settings.OAuthSettings.ClientSecret = clientSecret
}
if payload.AllowBindMountsForRegularUsers != nil {
+4
View File
@@ -41,6 +41,10 @@ func (handler *Handler) userDelete(w http.ResponseWriter, r *http.Request) *http
}
func (handler *Handler) deleteAdminUser(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError {
if user.Password == "" {
return handler.deleteUser(w, user)
}
users, err := handler.UserService.Users()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve users from the database", err}
+6 -1
View File
@@ -12,7 +12,8 @@ import (
// TODO: contain code related to legacy extension management
var extensionPorts = map[portainer.ExtensionID]string{
portainer.RegistryManagementExtension: "7001",
portainer.RegistryManagementExtension: "7001",
portainer.OAuthAuthenticationExtension: "7002",
}
type (
@@ -103,6 +104,10 @@ func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID)
return proxy, nil
}
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
return "http://localhost:" + extensionPorts[extensionID]
}
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID) {
manager.extensionProxies.Remove(strconv.Itoa(int(extensionID)))
+2 -2
View File
@@ -153,10 +153,10 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta
return true
}
// AuthorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
// authorizedEndpointGroupAccess ensure that the user can access the specified endpoint group.
// It will check if the user is part of the authorized users or part of a team that is
// listed in the authorized teams.
func AuthorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams)
}
+25
View File
@@ -111,6 +111,31 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain
return nil
}
// RegistryAccess retrieves the JWT token from the request context and verifies
// that the user can access the specified registry.
// An error is returned when access is denied.
func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portainer.Registry) error {
tokenData, err := RetrieveTokenData(r)
if err != nil {
return err
}
if tokenData.Role == portainer.AdministratorRole {
return nil
}
memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
if err != nil {
return err
}
if !AuthorizedRegistryAccess(registry, tokenData.ID, memberships) {
return portainer.ErrEndpointAccessDenied
}
return nil
}
// mwSecureHeaders provides secure headers middleware for handlers.
func mwSecureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+1 -1
View File
@@ -124,7 +124,7 @@ func FilterEndpointGroups(endpointGroups []portainer.EndpointGroup, context *Res
filteredEndpointGroups = make([]portainer.EndpointGroup, 0)
for _, group := range endpointGroups {
if AuthorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
if authorizedEndpointGroupAccess(&group, context.UserID, context.UserMemberships) {
filteredEndpointGroups = append(filteredEndpointGroups, group)
}
}
+2
View File
@@ -107,6 +107,8 @@ func (server *Server) Start() error {
authHandler.SettingsService = server.SettingsService
authHandler.TeamService = server.TeamService
authHandler.TeamMembershipService = server.TeamMembershipService
authHandler.ExtensionService = server.ExtensionService
authHandler.ProxyManager = proxyManager
var dockerHubHandler = dockerhub.NewHandler(requestBouncer)
dockerHubHandler.DockerHubService = server.DockerHubService
+25 -3
View File
@@ -56,6 +56,20 @@ type (
AutoCreateUsers bool `json:"AutoCreateUsers"`
}
// OAuthSettings represents the settings used to authorize with an authorization server
OAuthSettings struct {
ClientID string `json:"ClientID"`
ClientSecret string `json:"ClientSecret,omitempty"`
AccessTokenURI string `json:"AccessTokenURI"`
AuthorizationURI string `json:"AuthorizationURI"`
ResourceURI string `json:"ResourceURI"`
RedirectURI string `json:"RedirectURI"`
UserIdentifier string `json:"UserIdentifier"`
Scopes string `json:"Scopes"`
OAuthAutoCreateUsers bool `json:"OAuthAutoCreateUsers"`
DefaultTeamID TeamID `json:"DefaultTeamID"`
}
// TLSConfiguration represents a TLS configuration
TLSConfiguration struct {
TLS bool `json:"TLS"`
@@ -85,6 +99,7 @@ type (
BlackListedLabels []Pair `json:"BlackListedLabels"`
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
SnapshotInterval string `json:"SnapshotInterval"`
@@ -503,6 +518,7 @@ type (
LicenseKey string `json:"LicenseKey,omitempty"`
Company string `json:"Company,omitempty"`
Expiration string `json:"Expiration,omitempty"`
Valid bool `json:"Valid,omitempty"`
}
// CLIService represents a service for managing CLI
@@ -778,15 +794,17 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "1.20.0"
APIVersion = "1.20.2"
// DBVersion is the version number of the Portainer database
DBVersion = 16
DBVersion = 17
// AssetsServerURL represents the URL of the Portainer asset server
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
MessageOfTheDayURL = AssetsServerURL + "/motd.html"
// MessageOfTheDayTitleURL represents the URL where Portainer MOTD title can be retrieved
MessageOfTheDayTitleURL = AssetsServerURL + "/motd-title.txt"
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
ExtensionDefinitionsURL = AssetsServerURL + "/extensions.json"
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.20.2.json"
// PortainerAgentHeader represents the name of the header available in any agent response
PortainerAgentHeader = "Portainer-Agent"
// PortainerAgentTargetHeader represent the name of the header containing the target node name
@@ -833,6 +851,8 @@ const (
AuthenticationInternal
// AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server)
AuthenticationLDAP
//AuthenticationOAuth represents the OAuth authentication method (authentication against a authorization server)
AuthenticationOAuth
)
const (
@@ -911,6 +931,8 @@ const (
_ ExtensionID = iota
// RegistryManagementExtension represents the registry management extension
RegistryManagementExtension
// OAuthAuthenticationExtension represents the OAuth authentication extension
OAuthAuthenticationExtension
)
const (
+14 -9
View File
@@ -54,7 +54,7 @@ info:
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
version: "1.20.0"
version: "1.20.2"
title: "Portainer API"
contact:
email: "info@portainer.io"
@@ -525,7 +525,7 @@ paths:
**Access policy**: administrator
operationId: "EndpointJob"
consumes:
- "application/json"
- "multipart/form-data"
produces:
- "application/json"
security:
@@ -1434,7 +1434,7 @@ paths:
**Access policy**: restricted
operationId: "StackCreate"
consumes:
- "application/json"
- "multipart/form-data"
produces:
- "application/json"
security:
@@ -2733,7 +2733,7 @@ paths:
- "application/json"
security:
- jwt: []
parameters:
parameters: []
responses:
200:
description: "Success"
@@ -3018,7 +3018,7 @@ definitions:
description: "Is analytics enabled"
Version:
type: "string"
example: "1.20.0"
example: "1.20.2"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"
@@ -3146,7 +3146,7 @@ definitions:
$ref: "#/definitions/LDAPGroupSearchSettings"
AutoCreateUsers:
type: "boolean"
example: "true"
example: true
description: "Automatically provision users and assign them to matching LDAP group names"
Settings:
@@ -3606,6 +3606,7 @@ definitions:
- "Authentication"
- "Name"
- "Password"
- "Type"
- "URL"
- "Username"
properties:
@@ -3613,6 +3614,10 @@ definitions:
type: "string"
example: "my-registry"
description: "Name that will be used to identify this registry"
Type:
type: "integer"
example: 1
description: "Registry Type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)"
URL:
type: "string"
example: "registry.mydomain.tld:2375"
@@ -4037,7 +4042,7 @@ definitions:
description: "A list of categories associated to the template"
items:
type: "string"
exampe: "database"
example: "database"
registry:
type: "string"
example: "quay.io"
@@ -4133,7 +4138,7 @@ definitions:
description: "A list of categories associated to the template"
items:
type: "string"
exampe: "database"
example: "database"
registry:
type: "string"
example: "quay.io"
@@ -4233,7 +4238,7 @@ definitions:
description: "A list of categories associated to the template"
items:
type: "string"
exampe: "database"
example: "database"
registry:
type: "string"
example: "quay.io"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"packageName": "portainer",
"packageVersion": "1.20.0",
"packageVersion": "1.20.2",
"projectName": "portainer"
}
+2 -2
View File
@@ -30,7 +30,7 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin
};
$transitions.onBefore({ to: 'docker.**' }, function() {
HttpRequestHelper.resetAgentTargetQueue();
HttpRequestHelper.resetAgentHeaders();
});
}]);
@@ -45,7 +45,7 @@ function initAuthentication(authManager, Authentication, $rootScope, $state) {
// to have more controls on which URL should trigger the unauthenticated state.
$rootScope.$on('unauthenticated', function (event, data) {
if (!_.includes(data.config.url, '/v2/')) {
$state.go('portainer.auth', {error: 'Your session has expired'});
$state.go('portainer.auth', { error: 'Your session has expired' });
}
});
}
+3
View File
@@ -27,6 +27,9 @@ angular.module('portainer')
request: function(config) {
if (config.url.indexOf('/docker/') > -1) {
config.headers['X-PortainerAgent-Target'] = HttpRequestHelper.portainerAgentTargetHeader();
if (HttpRequestHelper.portainerAgentManagerOperation()) {
config.headers['X-PortainerAgent-ManagerOperation'] = '1';
}
}
return config;
}
+2 -1
View File
@@ -20,4 +20,5 @@ angular.module('portainer')
.constant('DEFAULT_TEMPLATES_URL', 'https://raw.githubusercontent.com/portainer/templates/master/templates.json')
.constant('PAGINATION_MAX_ITEMS', 10)
.constant('APPLICATION_CACHE_VALIDITY', 3600)
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.');
.constant('CONSOLE_COMMANDS_LABEL_PREFIX', 'io.portainer.commands.')
.constant('PREDEFINED_NETWORKS', ['host', 'bridge', 'none']);
+11 -1
View File
@@ -5,7 +5,17 @@ angular.module('portainer.docker', ['portainer.app'])
var docker = {
name: 'docker',
parent: 'root',
abstract: true
abstract: true,
resolve: {
endpointID: ['EndpointProvider', '$state',
function (EndpointProvider, $state) {
var id = EndpointProvider.endpointID();
if (!id) {
return $state.go('portainer.home');
}
}
]
}
};
var configs = {
@@ -38,12 +38,12 @@
</thead>
<tbody>
<tr dir-paginate="(key, value) in $ctrl.dataset | itemsPerPage: $ctrl.state.paginatedItemLimit" ng-class="{active: item.Checked}">
<td><a ui-sref="docker.networks.network({ id: value.NetworkID, nodeName: $ctrl.nodeName })">{{ key }}</a></td>
<td><a ui-sref="docker.networks.network({ id: key, nodeName: $ctrl.nodeName })">{{ key }}</a></td>
<td>{{ value.IPAddress || '-' }}</td>
<td>{{ value.Gateway || '-' }}</td>
<td>{{ value.MacAddress || '-' }}</td>
<td>
<button type="button" class="btn btn-xs btn-danger" ng-disabled="$ctrl.leaveNetworkActionInProgress" button-spinner="$ctrl.leaveNetworkActionInProgress" ng-click="$ctrl.leaveNetworkAction($ctrl.container, value.NetworkID)">
<button type="button" class="btn btn-xs btn-danger" ng-disabled="$ctrl.leaveNetworkActionInProgress" button-spinner="$ctrl.leaveNetworkActionInProgress" ng-click="$ctrl.leaveNetworkAction($ctrl.container, key)">
<span ng-hide="$ctrl.leaveNetworkActionInProgress"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i> Leave network</span>
<span ng-show="$ctrl.leaveNetworkActionInProgress">Leaving network...</span>
</button>
@@ -110,7 +110,7 @@
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" ng-disabled="$ctrl.disableRemove(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
@@ -1,6 +1,6 @@
angular.module('portainer.docker').component('networksDatatable', {
templateUrl: 'app/docker/components/datatables/networks-datatable/networksDatatable.html',
controller: 'GenericDatatableController',
controller: 'NetworksDatatableController',
bindings: {
titleText: '@',
titleIcon: '@',
@@ -0,0 +1,20 @@
angular.module('portainer.docker')
.controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS',
function ($scope, $controller, PREDEFINED_NETWORKS) {
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
this.disableRemove = function(item) {
return PREDEFINED_NETWORKS.includes(item.Name);
};
this.selectAll = function() {
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
var item = this.state.filteredDataSet[i];
if (!this.disableRemove(item) && item.Checked !== this.state.selectAll) {
item.Checked = this.state.selectAll;
this.selectItem(item);
}
}
};
}
]);
@@ -79,6 +79,13 @@
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Mountpoint' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th>
<a ng-click="$ctrl.changeOrderBy('CreatedAt')">
Created
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreatedAt' && !$ctrl.state.reverseOrder"></i>
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'CreatedAt' && $ctrl.state.reverseOrder"></i>
</a>
</th>
<th ng-if="$ctrl.showHostColumn">
<a ng-click="$ctrl.changeOrderBy('NodeName')">
Host
@@ -112,6 +119,7 @@
<td>{{ item.StackName ? item.StackName : '-' }}</td>
<td>{{ item.Driver }}</td>
<td>{{ item.Mountpoint | truncatelr }}</td>
<td>{{ item.CreatedAt | getisodate }}</td>
<td ng-if="$ctrl.showHostColumn">{{ item.NodeName ? item.NodeName : '-' }}</td>
<td ng-if="$ctrl.showOwnershipColumn">
<span>
+2 -3
View File
@@ -1,10 +1,9 @@
angular.module('portainer.docker')
.factory('ContainerHelper', [function ContainerHelperFactory() {
angular.module('portainer.docker').factory('ContainerHelper', [function ContainerHelperFactory() {
'use strict';
var helper = {};
helper.commandStringToArray = function(command) {
return splitargs(command, undefined, true);
return splitargs(command);
};
helper.commandArrayToString = function(array) {
+1
View File
@@ -1,5 +1,6 @@
function VolumeViewModel(data) {
this.Id = data.Name;
this.CreatedAt = data.CreatedAt;
this.Driver = data.Driver;
this.Options = data.Options;
this.Labels = data.Labels;
@@ -50,6 +50,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
PortBindings: [],
PublishAllPorts: false,
Binds: [],
AutoRemove: false,
NetworkMode: 'bridge',
Privileged: false,
Runtime: '',
@@ -493,6 +494,19 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
$scope.formValues.capabilities.push(new ContainerCapability(cap, false));
});
}
function hasCapability(item) {
return item.capability === cap.capability;
}
var capabilities = new ContainerCapabilities();
for (var i = 0; i < capabilities.length; i++) {
var cap = capabilities[i];
if (!_.find($scope.formValues.capabilities, hasCapability)) {
$scope.formValues.capabilities.push(cap);
}
}
$scope.formValues.capabilities.sort(function(a, b) {
return a.capability < b.capability ? -1 : 1;
});
@@ -509,6 +523,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
$scope.fromContainer = fromContainer;
$scope.config = ContainerHelper.configFromContainer(fromContainer.Model);
loadFromContainerCmd(d);
loadFromContainerLogging(d);
loadFromContainerPortBindings(d);
loadFromContainerVolumes(d);
loadFromContainerNetworkConfig(d);
@@ -525,6 +540,17 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
});
}
function loadFromContainerLogging(config) {
var logConfig = config.HostConfig.LogConfig;
$scope.formValues.LogDriverName = logConfig.Type;
$scope.formValues.LogDriverOpts = _.map(logConfig.Config, function (value, name) {
return {
name: name,
value: value
};
});
}
function initView() {
var nodeName = $transition$.params().nodeName;
$scope.formValues.NodeName = nodeName;
@@ -621,9 +647,9 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
function create() {
var oldContainer = null;
HttpRequestHelper.setPortainerAgentTargetHeader($scope.formValues.NodeName);
return findCurrentContainer()
.then(setOldContainer)
.then(confirmCreateContainer)
.then(startCreationProcess)
.catch(notifyOnError)
@@ -633,6 +659,11 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
$scope.state.actionInProgress = false;
}
function setOldContainer(container) {
oldContainer = container;
return container;
}
function findCurrentContainer() {
return Container.query({ all: 1, filters: { name: ['^/' + $scope.config.name + '$'] } })
.$promise
@@ -640,8 +671,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
if (!containers.length) {
return;
}
oldContainer = containers[0];
return oldContainer;
return containers[0];
})
.catch(notifyOnError);
@@ -659,12 +689,41 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
}
$scope.state.actionInProgress = true;
return pullImageIfNeeded()
.then(stopAndRenameContainer(oldContainer))
.then(stopAndRenameContainer)
.then(createNewContainer)
.then(applyResourceControl)
.then(connectToExtraNetworks)
.then(removeOldContainer)
.then(onSuccess);
.then(onSuccess)
.catch(onCreationProcessFail);
}
function onCreationProcessFail(error) {
var deferred = $q.defer();
removeNewContainer()
.then(restoreOldContainerName)
.then(function() {
deferred.reject(error);
})
.catch(function(restoreError) {
deferred.reject(restoreError);
});
return deferred.promise;
}
function removeNewContainer() {
return findCurrentContainer().then(function onContainerLoaded(container) {
if (container && (!oldContainer || container.Id !== oldContainer.Id)) {
return ContainerService.remove(container, true);
}
});
}
function restoreOldContainerName() {
if (!oldContainer) {
return;
}
return ContainerService.renameContainer(oldContainer.Id, oldContainer.Names[0].substring(1));
}
function confirmCreateContainer(container) {
@@ -695,7 +754,7 @@ function ($q, $scope, $state, $timeout, $transition$, $filter, Container, Contai
}
}
function stopAndRenameContainer(oldContainer) {
function stopAndRenameContainer() {
if (!oldContainer) {
return $q.when();
}
@@ -124,6 +124,19 @@
<div class="col-sm-12 form-section-title">
Actions
</div>
<!-- autoremove -->
<div class="form-group">
<div class="col-sm-12">
<label for="ownership" class="control-label text-left">
Auto remove
<portainer-tooltip position="bottom" message="When enabled, Portainer will automatically remove the container when it exits. This is useful when you want to use the container only once."></portainer-tooltip>
</label>
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" ng-model="config.HostConfig.AutoRemove"><i></i>
</label>
</div>
</div>
<!-- !autoremove -->
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !config.Image || (!formValues.Registry && fromContainer)" ng-click="create()" button-spinner="state.actionInProgress">
@@ -162,7 +175,7 @@
<form class="form-horizontal" style="margin-top: 15px;">
<!-- command-input -->
<div class="form-group">
<label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command & logging</label>
<label for="container_command" class="col-sm-2 col-lg-1 control-label text-left">Command</label>
<div class="col-sm-9">
<input type="text" class="form-control" ng-model="config.Cmd" id="container_command" placeholder="e.g. /usr/bin/nginx -t -c /mynginx.conf">
</div>
@@ -320,7 +333,7 @@
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.name">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}}</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Name">{{ vol.Name|truncate:30}} - {{ vol.Driver|truncate:30}}</option>
</select>
</div>
<!-- !volume -->
@@ -126,6 +126,7 @@ angular.module('portainer.docker')
function createNetwork(context) {
HttpRequestHelper.setPortainerAgentTargetHeader(context.nodeName);
HttpRequestHelper.setPortainerAgentManagerOperation(context.managerOperation);
$scope.state.actionInProgress = true;
NetworkService.create(context.networkConfiguration)
@@ -162,12 +163,17 @@ angular.module('portainer.docker')
var creationContext = {
nodeName: $scope.formValues.NodeName,
managerOperation: false,
networkConfiguration: networkConfiguration,
userDetails: userDetails,
accessControlData: accessControlData,
reload: true
};
if ($scope.applicationState.endpoint.mode.agentProxy && $scope.applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && $scope.config.Driver === 'overlay') {
creationContext.managerOperation = true;
}
if ($scope.config.Driver === 'macvlan') {
if ($scope.formValues.Macvlan.Scope === 'local') {
modifyNetworkConfigurationForMacvlanConfigOnly(networkConfiguration);
@@ -205,4 +211,4 @@ angular.module('portainer.docker')
initView();
}
]);
]);
+4 -8
View File
@@ -20,7 +20,7 @@
<td>ID</td>
<td>
{{ network.Id }}
<button class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this network</button>
<button ng-if="allowRemove(network)" class="btn btn-xs btn-danger" ng-click="removeNetwork(network.Id)"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Delete this network</button>
</td>
</tr>
<tr>
@@ -39,13 +39,9 @@
<td>Internal</td>
<td>{{ network.Internal }}</td>
</tr>
<tr ng-if="network.IPAM.Config[0].Subnet">
<td>Subnet</td>
<td>{{ network.IPAM.Config[0].Subnet }}</td>
</tr>
<tr ng-if="network.IPAM.Config[0].Gateway">
<td>Gateway</td>
<td>{{ network.IPAM.Config[0].Gateway }}</td>
<tr ng-if="network.IPAM.Config.length > 0" ng-repeat="config in network.IPAM.Config">
<td>Subnet - {{ config.Subnet }}</td>
<td>Gateway - {{ config.Gateway }}</td>
</tr>
</tbody>
</table>
@@ -1,6 +1,6 @@
angular.module('portainer.docker')
.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper',
function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper) {
.controller('NetworkController', ['$scope', '$state', '$transition$', '$filter', 'NetworkService', 'Container', 'Notifications', 'HttpRequestHelper', 'PREDEFINED_NETWORKS',
function ($scope, $state, $transition$, $filter, NetworkService, Container, Notifications, HttpRequestHelper, PREDEFINED_NETWORKS) {
$scope.removeNetwork = function removeNetwork() {
NetworkService.remove($transition$.params().id, $transition$.params().id)
@@ -25,6 +25,10 @@ function ($scope, $state, $transition$, $filter, NetworkService, Container, Noti
});
};
$scope.allowRemove = function allowRemove(item) {
return !PREDEFINED_NETWORKS.includes(item.Name);
};
function filterContainersInNetwork(network, containers) {
var containersInNetwork = [];
containers.forEach(function(container) {
@@ -315,8 +315,9 @@
<!-- volume -->
<div class="input-group input-group-sm col-sm-6" ng-if="volume.Type === 'volume'">
<span class="input-group-addon">volume</span>
<select class="form-control" ng-model="volume.Source" ng-options="vol.Id|truncate:30 for vol in availableVolumes">
<select class="form-control" ng-model="volume.Source">
<option selected disabled hidden value="">Select a volume</option>
<option ng-repeat="vol in availableVolumes" ng-value="vol.Id">{{ vol.Id|truncate:30}} - {{ vol.Driver|truncate:30}}</option>
</select>
</div>
<!-- !volume -->
@@ -19,6 +19,10 @@
<button class="btn btn-xs btn-danger" ng-click="removeVolume()"><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove this volume</button>
</td>
</tr>
<tr>
<td>Created</td>
<td>{{ volume.CreatedAt | getisodate }}</td>
</tr>
<tr>
<td>Mount path</td>
<td>{{ volume.Mountpoint }}</td>
+2 -1
View File
@@ -1,3 +1,4 @@
angular.module('portainer.extensions', [
'portainer.extensions.registrymanagement'
'portainer.extensions.registrymanagement',
'portainer.extensions.oauth'
]);
+2
View File
@@ -0,0 +1,2 @@
angular.module('portainer.extensions.oauth', ['ngResource'])
.constant('API_ENDPOINT_OAUTH', 'api/auth/oauth');
@@ -0,0 +1,63 @@
angular.module('portainer.extensions.oauth')
.controller('OAuthProviderSelectorController', function OAuthProviderSelectorController() {
var ctrl = this;
this.providers = [
{
authUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/authorize',
accessTokenUrl: 'https://login.microsoftonline.com/TENANT_ID/oauth2/token',
resourceUrl: 'https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08',
userIdentifier: 'userPrincipalName',
scopes: 'id,email,name',
name: 'microsoft'
},
{
authUrl: 'https://accounts.google.com/o/oauth2/auth',
accessTokenUrl: 'https://accounts.google.com/o/oauth2/token',
resourceUrl: 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json',
userIdentifier: 'email',
scopes: 'profile email',
name: 'google'
},
{
authUrl: 'https://github.com/login/oauth/authorize',
accessTokenUrl: 'https://github.com/login/oauth/access_token',
resourceUrl: 'https://api.github.com/user',
userIdentifier: 'login',
scopes: 'id email name',
name: 'github'
},
{
authUrl: '',
accessTokenUrl: '',
resourceUrl: '',
userIdentifier: '',
scopes: '',
name: 'custom'
}
];
this.$onInit = onInit;
function onInit() {
if (ctrl.provider.authUrl) {
ctrl.provider = getProviderByURL(ctrl.provider.authUrl);
} else {
ctrl.provider = ctrl.providers[0];
}
ctrl.onSelect(ctrl.provider, false);
}
function getProviderByURL(providerAuthURL) {
if (providerAuthURL.indexOf('login.microsoftonline.com') !== -1) {
return ctrl.providers[0];
}
else if (providerAuthURL.indexOf('accounts.google.com') !== -1) {
return ctrl.providers[1];
}
else if (providerAuthURL.indexOf('github.com') !== -1) {
return ctrl.providers[2];
}
return ctrl.providers[3];
}
});
@@ -0,0 +1,49 @@
<div class="col-sm-12 form-section-title">
Provider
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
<input type="radio" id="oauth_provider_microsoft" ng-model="$ctrl.provider" ng-value="$ctrl.providers[0]">
<label for="oauth_provider_microsoft">
<div class="boxselector_header">
<i class="fab fa-microsoft" aria-hidden="true" style="margin-right: 2px;"></i>
Microsoft
</div>
<p>Microsoft OAuth provider</p>
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
<input type="radio" id="oauth_provider_google" ng-model="$ctrl.provider" ng-value="$ctrl.providers[1]">
<label for="oauth_provider_google">
<div class="boxselector_header">
<i class="fab fa-google" aria-hidden="true" style="margin-right: 2px;"></i>
Google
</div>
<p>Google OAuth provider</p>
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
<input type="radio" id="oauth_provider_github" ng-model="$ctrl.provider" ng-value="$ctrl.providers[2]">
<label for="oauth_provider_github">
<div class="boxselector_header">
<i class="fab fa-github" aria-hidden="true" style="margin-right: 2px;"></i>
Github
</div>
<p>Github OAuth provider</p>
</label>
</div>
<div ng-click="$ctrl.onSelect($ctrl.provider, true)">
<input type="radio" id="oauth_provider_custom" ng-model="$ctrl.provider" ng-value="$ctrl.providers[3]">
<label for="oauth_provider_custom">
<div class="boxselector_header">
<i class="fa fa-user-check" aria-hidden="true" style="margin-right: 2px;"></i>
Custom
</div>
<p>Custom OAuth provider</p>
</label>
</div>
</div>
</div>
@@ -0,0 +1,8 @@
angular.module('portainer.extensions.oauth').component('oauthProvidersSelector', {
templateUrl: 'app/extensions/oauth/components/oauth-providers-selector/oauth-providers-selector.html',
bindings: {
onSelect: '<',
provider: '='
},
controller: 'OAuthProviderSelectorController'
});
@@ -0,0 +1,74 @@
angular.module('portainer.extensions.oauth')
.controller('OAuthSettingsController', function OAuthSettingsController() {
var ctrl = this;
this.state = {
provider: {},
overrideConfiguration: false,
microsoftTenantID: ''
};
this.$onInit = onInit;
this.onSelectProvider = onSelectProvider;
this.onMicrosoftTenantIDChange = onMicrosoftTenantIDChange;
this.useDefaultProviderConfiguration = useDefaultProviderConfiguration;
function onMicrosoftTenantIDChange() {
var tenantID = ctrl.state.microsoftTenantID;
ctrl.settings.AuthorizationURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/authorize', 'TENANT_ID', tenantID);
ctrl.settings.AccessTokenURI = _.replace('https://login.microsoftonline.com/TENANT_ID/oauth2/token', 'TENANT_ID', tenantID);
ctrl.settings.ResourceURI = _.replace('https://graph.windows.net/TENANT_ID/me?api-version=2013-11-08', 'TENANT_ID', tenantID);
}
function useDefaultProviderConfiguration() {
ctrl.settings.AuthorizationURI = ctrl.state.provider.authUrl;
ctrl.settings.AccessTokenURI = ctrl.state.provider.accessTokenUrl;
ctrl.settings.ResourceURI = ctrl.state.provider.resourceUrl;
ctrl.settings.UserIdentifier = ctrl.state.provider.userIdentifier;
ctrl.settings.Scopes = ctrl.state.provider.scopes;
if (ctrl.state.provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
onMicrosoftTenantIDChange();
}
}
function useExistingConfiguration() {
var provider = ctrl.state.provider;
ctrl.settings.AuthorizationURI = ctrl.settings.AuthorizationURI === '' ? provider.authUrl : ctrl.settings.AuthorizationURI;
ctrl.settings.AccessTokenURI = ctrl.settings.AccessTokenURI === '' ? provider.accessTokenUrl : ctrl.settings.AccessTokenURI;
ctrl.settings.ResourceURI = ctrl.settings.ResourceURI === '' ? provider.resourceUrl : ctrl.settings.ResourceURI;
ctrl.settings.UserIdentifier = ctrl.settings.UserIdentifier === '' ? provider.userIdentifier : ctrl.settings.UserIdentifier;
ctrl.settings.Scopes = ctrl.settings.Scopes === '' ? provider.scopes : ctrl.settings.Scopes;
if (provider.name === 'microsoft' && ctrl.state.microsoftTenantID !== '') {
onMicrosoftTenantIDChange();
}
}
function onSelectProvider(provider, overrideConfiguration) {
ctrl.state.provider = provider;
if (overrideConfiguration) {
useDefaultProviderConfiguration();
} else {
useExistingConfiguration();
}
}
function onInit() {
if (ctrl.settings.RedirectURI === '') {
ctrl.settings.RedirectURI = window.location.origin;
}
if (ctrl.settings.AuthorizationURI !== '') {
ctrl.state.provider.authUrl = ctrl.settings.AuthorizationURI;
if (ctrl.settings.AuthorizationURI.indexOf('login.microsoftonline.com') > -1) {
var tenantID = ctrl.settings.AuthorizationURI.match(/login.microsoftonline.com\/(.*?)\//)[1];
ctrl.state.microsoftTenantID = tenantID;
onMicrosoftTenantIDChange();
}
}
}
});
@@ -0,0 +1,215 @@
<div>
<div class="col-sm-12 form-section-title">
Automatic user provisioning
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
With automatic user provisioning enabled, Portainer will create user(s) automatically with standard user role. If
disabled, users must be created beforehand in Portainer in order to login.
</span>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">Automatic user provisioning</label>
<label class="switch" style="margin-left: 20px">
<input type="checkbox" ng-model="$ctrl.settings.OAuthAutoCreateUsers" /><i></i>
</label>
</div>
<div ng-if="$ctrl.settings.OAuthAutoCreateUsers">
<div class="form-group">
<span class="col-sm-12 text-muted small">
<p>The users created by the automatic provisioning feature can be added to a default team on creation.</p>
<p>By assigning newly created users to a team they will be able to access the environments associated to that team. This setting is optional and if not set newly created users won't be able to access any environments.</p>
</span>
</div>
<div class="form-group">
<label class="col-sm-3 col-lg-2 control-label text-left">Default team</label>
<span class="small text-muted" style="margin-left: 20px;" ng-if="$ctrl.teams.length === 0">
You have not yet created any team. Head over the <a ui-sref="portainer.teams">teams view</a> to manage user teams.
</span>
<button type="button" class="btn btn-sm btn-danger" ng-click="$ctrl.settings.DefaultTeamID = null" ng-disabled="!$ctrl.settings.DefaultTeamID" ng-if="$ctrl.teams.length > 0"><i class="fa fa-times" aria-hidden="true"></i></button>
<div class="col-sm-9 col-lg-9" ng-if="$ctrl.teams.length > 0">
<select class="form-control" ng-model="$ctrl.settings.DefaultTeamID" ng-options="team.Id as team.Name for team in $ctrl.teams">
<option value="">No team</option>
</select>
</div>
</div>
</div>
<oauth-providers-selector on-select="$ctrl.onSelectProvider" provider="$ctrl.state.provider"></oauth-providers-selector>
<div class="col-sm-12 form-section-title">OAuth Configuration</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'microsoft'">
<label for="oauth_microsoft_tenant_id" class="col-sm-3 col-lg-2 control-label text-left">
Tenant ID
<portainer-tooltip position="bottom" message="ID of the Azure Directory you wish to authenticate against. Also known as the Directory ID"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_microsoft_tenant_id"
placeholder="xxxxxxxxxxxxxxxxxxxx"
ng-model="$ctrl.state.microsoftTenantID"
ng-change="$ctrl.onMicrosoftTenantIDChange()"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_client_id" class="col-sm-3 col-lg-2 control-label text-left">
{{ $ctrl.state.provider.name == 'microsoft' ? 'Application ID' : 'Client ID' }}
<portainer-tooltip position="bottom" message="Public identifier of the OAuth application"></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_client_id"
ng-model="$ctrl.settings.ClientID"
placeholder="xxxxxxxxxxxxxxxxxxxx"
/>
</div>
</div>
<div class="form-group">
<label for="oauth_client_secret" class="col-sm-3 col-lg-2 control-label text-left">
{{ $ctrl.state.provider.name == 'microsoft' ? 'Application key' : 'Client secret' }}
</label>
<div class="col-sm-9 col-lg-10">
<input
type="password"
class="form-control"
id="oauth_client_secret"
ng-model="$ctrl.settings.ClientSecret"
placeholder="xxxxxxxxxxxxxxxxxxxx"
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_authorization_uri" class="col-sm-3 col-lg-2 control-label text-left">
Authorization URL
<portainer-tooltip
position="bottom"
message="URL used to authenticate against the OAuth provider. Will redirect the user to the OAuth provider login view"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_authorization_uri"
ng-model="$ctrl.settings.AuthorizationURI"
placeholder="https://example.com/oauth/authorize"
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_access_token_uri" class="col-sm-3 col-lg-2 control-label text-left">
Access token URL
<portainer-tooltip
position="bottom"
message="URL used by Portainer to exchange a valid OAuth authentication code for an access token"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_access_token_uri"
ng-model="$ctrl.settings.AccessTokenURI"
placeholder="https://example.com/oauth/token"
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_resource_uri" class="col-sm-3 col-lg-2 control-label text-left">
Resource URL
<portainer-tooltip
position="bottom"
message="URL used by Portainer to retrieve information about the authenticated user"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_resource_uri"
ng-model="$ctrl.settings.ResourceURI"
placeholder="https://example.com/user"
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_redirect_uri" class="col-sm-3 col-lg-2 control-label text-left">
Redirect URL
<portainer-tooltip
position="bottom"
message="URL used by the OAuth provider to redirect the user after successful authentication. Should be set to your Portainer instance URL"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_redirect_uri"
ng-model="$ctrl.settings.RedirectURI"
placeholder="http://yourportainer.com/"
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_user_identifier" class="col-sm-3 col-lg-2 control-label text-left">
User identifier
<portainer-tooltip
position="bottom"
message="Identifier that will be used by Portainer to create an account for the authenticated user. Retrieved from the resource server specified via the Resource URL field"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_user_identifier"
ng-model="$ctrl.settings.UserIdentifier"
placeholder="id"
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name == 'custom' || $ctrl.state.overrideConfiguration">
<label for="oauth_scopes" class="col-sm-3 col-lg-2 control-label text-left">
Scopes
<portainer-tooltip
position="bottom"
message="Scopes required by the OAuth provider to retrieve information about the authenticated user. Refer to your OAuth provider documentation for more information about this"
></portainer-tooltip>
</label>
<div class="col-sm-9 col-lg-10">
<input
type="text"
class="form-control"
id="oauth_scopes"
ng-model="$ctrl.settings.Scopes"
placeholder="id,email,name"
/>
</div>
</div>
<div class="form-group" ng-if="$ctrl.state.provider.name != 'custom'">
<div class="col-sm-12">
<a class="small interactive" ng-if="!$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = true;">
<i class="fa fa-wrench space-right" aria-hidden="true"></i> Override default configuration
</a>
<a class="small interactive" ng-if="$ctrl.state.overrideConfiguration" ng-click="$ctrl.state.overrideConfiguration = false; $ctrl.useDefaultProviderConfiguration()">
<i class="fa fa-cogs space-right" aria-hidden="true"></i> Use default configuration
</a>
</div>
</div>
</div>
@@ -0,0 +1,8 @@
angular.module('portainer.extensions.oauth').component('oauthSettings', {
templateUrl: 'app/extensions/oauth/components/oauth-settings/oauth-settings.html',
bindings: {
settings: '=',
teams: '<'
},
controller: 'OAuthSettingsController'
});
@@ -0,0 +1,13 @@
angular.module('portainer.extensions.oauth')
.factory('OAuth', ['$resource', 'API_ENDPOINT_OAUTH', function OAuthFactory($resource, API_ENDPOINT_OAUTH) {
'use strict';
return $resource(API_ENDPOINT_OAUTH + '/:action', {}, {
validate: {
method: 'POST',
ignoreLoadingBar: true,
params: {
action: 'validate'
}
}
});
}]);
@@ -1,11 +1,13 @@
angular.module('portainer.extensions.registrymanagement')
.factory('RegistryCatalog', ['$resource', 'API_ENDPOINT_REGISTRIES', function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) {
.factory('RegistryCatalog', ['$resource', 'API_ENDPOINT_REGISTRIES',
function RegistryCatalogFactory($resource, API_ENDPOINT_REGISTRIES) {
'use strict';
return $resource(API_ENDPOINT_REGISTRIES + '/:id/v2/:action', {},
{
get: {
method: 'GET',
params: { id: '@id', action: '_catalog' }
params: { id: '@id', action: '_catalog' },
transformResponse: linkGetResponse
},
ping: {
method: 'GET',
@@ -0,0 +1,13 @@
function linkGetResponse(data, headers) {
var response = angular.fromJson(data);
var link = headers('link');
if (link) {
var queryString = link.substring(link.indexOf('?') + 1).split('>;')[0];
var queries = queryString.split('&');
for (var i = 0; i < queries.length; i++) {
var kv = queries[i].split('=');
response[kv[0]] = kv[1];
}
}
return response;
}
@@ -11,16 +11,33 @@ function RegistryV2ServiceFactory($q, RegistryCatalog, RegistryTags, RegistryMan
return RegistryCatalog.ping({ id: id }).$promise;
};
function getCatalog(id) {
var deferred = $q.defer();
var repositories = [];
_getCatalogPage({id: id}, deferred, repositories);
return deferred.promise;
}
function _getCatalogPage(params, deferred, repositories) {
RegistryCatalog.get(params).$promise.then(function(data) {
repositories = _.concat(repositories, data.repositories);
if (data.last && data.n) {
_getCatalogPage({id: params.id, n: data.n, last: data.last}, deferred, repositories);
} else {
deferred.resolve(repositories);
}
});
}
service.repositories = function (id) {
var deferred = $q.defer();
RegistryCatalog.get({
id: id
}).$promise
.then(function success(data) {
getCatalog(id).then(function success(data) {
var promises = [];
for (var i = 0; i < data.repositories.length; i++) {
var repository = data.repositories[i];
for (var i = 0; i < data.length; i++) {
var repository = data[i];
promises.push(RegistryTags.get({
id: id,
repository: repository
@@ -81,9 +81,17 @@ angular.module('portainer.app')
});
return $q.all(promises);
})
.then(function success() {
.then(function success(data) {
Notifications.success('Success', 'Tags successfully deleted');
$state.reload();
if (data.length === 0) {
$state.go('portainer.registries.registry.repositories', {
id: $scope.registryId
}, {
reload: true
});
} else {
$state.reload();
}
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to delete tags');
@@ -127,9 +135,9 @@ angular.module('portainer.app')
})
.then(function success(data) {
$scope.registry = data.registry;
$scope.repository.Tags = data.tags;
$scope.repository.Tags = [].concat(data.tags || []);
$scope.tags = [];
for (var i = 0; i < data.tags.length; i++) {
for (var i = 0; i < $scope.repository.Tags.length; i++) {
var tag = data.tags[i];
RegistryV2Service.tag(registryId, repository, tag)
.then(function success(data) {
@@ -5,7 +5,7 @@
</a>
</rd-header-title>
<rd-header-content>
<a ui-sref="portainer.registries">Registries</a> &gt; <a ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a> &gt; Repositories
<a ui-sref="portainer.registries">Registries</a> &gt; <a ng-if="isAdmin" ui-sref="portainer.registries.registry({id: registry.Id})">{{ registry.Name }}</a><span ng-if="!isAdmin">{{ registry.Name}}</span> &gt; Repositories
</rd-header-content>
</rd-header>
@@ -1,6 +1,6 @@
angular.module('portainer.extensions.registrymanagement')
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications',
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications) {
.controller('RegistryRepositoriesController', ['$transition$', '$scope', 'RegistryService', 'RegistryV2Service', 'Notifications', 'Authentication',
function ($transition$, $scope, RegistryService, RegistryV2Service, Notifications, Authentication) {
$scope.state = {
displayInvalidConfigurationMessage: false
@@ -9,6 +9,13 @@ function ($transition$, $scope, RegistryService, RegistryV2Service, Notification
function initView() {
var registryId = $transition$.params().id;
var authenticationEnabled = $scope.applicationState.application.authentication;
if (authenticationEnabled) {
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1;
$scope.isAdmin = isAdmin;
}
RegistryService.registry(registryId)
.then(function success(data) {
$scope.registry = data;
+24 -4
View File
@@ -87,7 +87,7 @@ angular.module('portainer.app', [])
}
};
var endpointCreation = {
var endpointCreation = {
name: 'portainer.endpoints.new',
url: '/new',
views: {
@@ -242,7 +242,7 @@ angular.module('portainer.app', [])
}
};
var registryCreation = {
var registryCreation = {
name: 'portainer.registries.new',
url: '/new',
views: {
@@ -286,7 +286,7 @@ angular.module('portainer.app', [])
}
};
var scheduleCreation = {
var scheduleCreation = {
name: 'portainer.schedules.new',
url: '/new',
views: {
@@ -327,6 +327,16 @@ angular.module('portainer.app', [])
templateUrl: 'app/portainer/views/stacks/stacks.html',
controller: 'StacksController'
}
},
resolve: {
endpointID: ['EndpointProvider', '$state',
function (EndpointProvider, $state) {
var id = EndpointProvider.endpointID();
if (!id) {
return $state.go('portainer.home');
}
}
]
}
};
@@ -342,7 +352,7 @@ angular.module('portainer.app', [])
};
var stackCreation = {
name: 'portainer.newstack',
name: 'portainer.stacks.newstack',
url: '/newstack',
views: {
'content@': {
@@ -447,6 +457,16 @@ angular.module('portainer.app', [])
var templates = {
name: 'portainer.templates',
url: '/templates',
resolve: {
endpointID: ['EndpointProvider', '$state',
function (EndpointProvider, $state) {
var id = EndpointProvider.endpointID();
if (!id) {
return $state.go('portainer.home');
}
}
]
},
views: {
'content@': {
templateUrl: 'app/portainer/views/templates/templates.html',
@@ -6,7 +6,7 @@
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
</div>
</div>
<div class="actionBar">
<div class="actionBar" ng-if="$ctrl.accessManagement">
<button type="button" class="btn btn-sm btn-danger"
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
@@ -24,7 +24,7 @@
<thead>
<tr>
<th>
<span class="md-checkbox">
<span class="md-checkbox" ng-if="$ctrl.accessManagement">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
@@ -47,11 +47,12 @@
<tbody>
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
<td>
<span class="md-checkbox">
<span class="md-checkbox" ng-if="$ctrl.accessManagement">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.registries.registry({id: item.Id})">{{ item.Name }}</a>
<a ui-sref="portainer.registries.registry({id: item.Id})" ng-if="$ctrl.accessManagement">{{ item.Name }}</a>
<span ng-if="!$ctrl.accessManagement">{{ item.Name }}</span>
<span ng-if="item.Authentication" style="margin-left: 5px;" class="label label-info image-tag">authentication-enabled</span>
</td>
<td>
@@ -11,7 +11,7 @@
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
</button>
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.newstack">
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.stacks.newstack">
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add stack
</button>
</div>
@@ -65,8 +65,9 @@
</span>
</td>
<td>
<span ng-if="item.Id === 1 || $ctrl.authenticationMethod !== 2">Internal</span>
<span ng-if="item.Id === 1 || $ctrl.authenticationMethod !== 2 && $ctrl.authenticationMethod !== 3">Internal</span>
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 2">LDAP</span>
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 3">OAuth</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
@@ -23,11 +23,11 @@
</span>
<span>
<span class="small">
<span class="small" ng-if="$ctrl.model.GroupName">
Group: {{ $ctrl.model.GroupName }}
</span>
<button
ng-if="$ctrl.isAdmin"
ng-if="$ctrl.isAdmin"
class="btn btn-link btn-xs"
ng-click="$ctrl.editEndpoint($event)"><i class="fa fa-pencil-alt"></i>
</button>
@@ -21,10 +21,10 @@
</span>
<span>
<span class="label label-primary" ng-if="!$ctrl.model.Enabled && !$ctrl.model.Available">coming soon</span>
<span class="label label-warning" ng-if="!$ctrl.model.Enabled && $ctrl.model.Deal">deal</span>
<span class="label label-danger" ng-if="$ctrl.model.Enabled && $ctrl.model.Expired">expired</span>
<span class="label label-success" ng-if="$ctrl.model.Enabled && !$ctrl.model.Expired">enabled</span>
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.UpdateAvailable && !$ctrl.model.Expired">update available</span>
<span class="label label-warning" ng-if="!$ctrl.model.Enabled && $ctrl.model.Deal && !$ctrl.model.License.Expiration">deal</span>
<span class="label label-danger" ng-if="!$ctrl.model.Enabled && $ctrl.model.License.Expiration && !$ctrl.model.License.Valid">expired</span>
<span class="label label-success" ng-if="$ctrl.model.Enabled && $ctrl.model.License.Valid">enabled</span>
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.License.Valid && $ctrl.model.UpdateAvailable">update available</span>
</span>
</div>
<!-- !blocklist-item-line1 -->
@@ -3,7 +3,6 @@ angular.module('portainer.app')
function($state) {
var ctrl = this;
ctrl.$onInit = $onInit;
ctrl.goToExtensionView = goToExtensionView;
function goToExtensionView() {
@@ -11,10 +10,4 @@ angular.module('portainer.app')
$state.go('portainer.extensions.extension', { id: ctrl.model.Id });
}
}
function $onInit() {
if (ctrl.currentDate === ctrl.model.License.Expiration) {
ctrl.model.Expired = true;
}
}
}]);
@@ -3,7 +3,7 @@
<div class="blocklist-item-box">
<!-- extension-image -->
<span class="blocklist-item-logo">
<img class="blocklist-item-logo" src="images/support_{{ $ctrl.model.Id }}.png" />
<img class="blocklist-item-logo" ng-src="images/support_{{ $ctrl.model.Id }}.png" />
</span>
<!-- !extension-image -->
<!-- extension-details -->
@@ -15,11 +15,6 @@
{{ $ctrl.model.Name }}
</span>
</span>
<span>
<span class="label label-danger" ng-if="$ctrl.model.Enabled && $ctrl.model.Expired">expired</span>
<span class="label label-success" ng-if="$ctrl.model.Enabled && !$ctrl.model.Expired">enabled</span>
<span class="label label-primary" ng-if="$ctrl.model.Enabled && $ctrl.model.UpdateAvailable && !$ctrl.model.Expired">update available</span>
</span>
</div>
<!-- !blocklist-item-line1 -->
<!-- blocklist-item-line2 -->
@@ -29,9 +24,6 @@
{{ $ctrl.model.ShortDescription }}
</span>
</span>
<span ng-if="$ctrl.model.License.Company">
<span class="small text-muted">Licensed to {{ $ctrl.model.License.Company }} - Expires on {{ $ctrl.model.License.Expiration }}</span>
</span>
</div>
<!-- !blocklist-item-line2 -->
</span>
+30
View File
@@ -0,0 +1,30 @@
angular.module('portainer.app')
.factory('URLHelper', ['$window', function URLHelperFactory($window) {
'use strict';
var helper = {};
helper.getParameter = getParameter;
helper.cleanParameters = cleanParameters;
function getParameter(param) {
var parameters = extractParameters();
return parameters[param];
}
function extractParameters() {
var queryString = $window.location.search.replace(/.*?\?/,'').split('&');
return queryString.reduce(function(acc, keyValStr) {
var keyVal = keyValStr.split('=');
var key = keyVal[0];
var val = keyVal[1];
acc[key] = val;
return acc;
}, {});
}
function cleanParameters() {
$window.location.search = '';
}
return helper;
}]);
@@ -1,5 +1,5 @@
angular.module('portainer.app')
.factory('EndpointStatusInterceptor', ['$q', '$injector', 'EndpointProvider', function ($q, $injector, EndpointProvider) {
.factory('EndpointStatusInterceptor', ['$q', 'EndpointProvider', function ($q, EndpointProvider) {
'use strict';
var interceptor = {};
@@ -18,21 +18,17 @@ angular.module('portainer.app')
}
function responseInterceptor(response) {
var EndpointService = $injector.get('EndpointService');
var url = response.config.url;
if (response.status === 200 && canBeOffline(url) && EndpointProvider.offlineMode()) {
EndpointProvider.setOfflineMode(false);
EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(false)});
}
return response || $q.when(response);
}
function responseErrorInterceptor(rejection) {
var EndpointService = $injector.get('EndpointService');
var url = rejection.config.url;
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
EndpointProvider.setOfflineMode(true);
EndpointService.updateEndpoint(EndpointProvider.endpointID(), {Status: EndpointProvider.endpointStatusFromOfflineMode(true)});
}
return $q.reject(rejection);
}
+1
View File
@@ -1,4 +1,5 @@
function MotdViewModel(data) {
this.Title = data.Title;
this.Message = data.Message;
this.Hash = data.Hash;
}
+24
View File
@@ -3,6 +3,7 @@ function SettingsViewModel(data) {
this.BlackListedLabels = data.BlackListedLabels;
this.AuthenticationMethod = data.AuthenticationMethod;
this.LDAPSettings = data.LDAPSettings;
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
this.SnapshotInterval = data.SnapshotInterval;
@@ -11,6 +12,16 @@ function SettingsViewModel(data) {
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
}
function PublicSettingsViewModel(settings) {
this.AllowBindMountsForRegularUsers = settings.AllowBindMountsForRegularUsers;
this.AllowPrivilegedModeForRegularUsers = settings.AllowPrivilegedModeForRegularUsers;
this.AuthenticationMethod = settings.AuthenticationMethod;
this.EnableHostManagementFeatures = settings.EnableHostManagementFeatures;
this.ExternalTemplates = settings.ExternalTemplates;
this.LogoURL = settings.LogoURL;
this.OAuthLoginURI = settings.OAuthLoginURI;
}
function LDAPSettingsViewModel(data) {
this.ReaderDN = data.ReaderDN;
this.Password = data.Password;
@@ -31,3 +42,16 @@ function LDAPGroupSearchSettings(GroupBaseDN, GroupAttribute, GroupFilter) {
this.GroupAttribute = GroupAttribute;
this.GroupFilter = GroupFilter;
}
function OAuthSettingsViewModel(data) {
this.ClientID = data.ClientID;
this.ClientSecret = data.ClientSecret;
this.AccessTokenURI = data.AccessTokenURI;
this.AuthorizationURI = data.AuthorizationURI;
this.ResourceURI = data.ResourceURI;
this.RedirectURI = data.RedirectURI;
this.UserIdentifier = data.UserIdentifier;
this.Scopes = data.Scopes;
this.OAuthAutoCreateUsers = data.OAuthAutoCreateUsers;
this.DefaultTeamID = data.DefaultTeamID;
}
@@ -62,5 +62,20 @@ angular.module('portainer.app')
return deferred.promise;
};
service.OAuthAuthenticationEnabled = function() {
var deferred = $q.defer();
service.extensions(false)
.then(function onSuccess(extensions) {
var extensionAvailable = _.find(extensions, { Id: 2, Enabled: true }) ? true : false;
deferred.resolve(extensionAvailable);
})
.catch(function onError(err) {
deferred.reject(err);
});
return deferred.promise;
};
return service;
}]);
@@ -27,7 +27,7 @@ angular.module('portainer.app')
Settings.publicSettings().$promise
.then(function success(data) {
var settings = new SettingsViewModel(data);
var settings = new PublicSettingsViewModel(data);
deferred.resolve(settings);
})
.catch(function error(err) {
+24 -21
View File
@@ -1,11 +1,14 @@
angular.module('portainer.app')
.factory('Authentication', ['$q', 'Auth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider', function AuthenticationFactory($q, Auth, jwtHelper, LocalStorage, StateManager, EndpointProvider) {
.factory('Authentication', [
'Auth', 'OAuth', 'jwtHelper', 'LocalStorage', 'StateManager', 'EndpointProvider',
function AuthenticationFactory(Auth, OAuth, jwtHelper, LocalStorage, StateManager, EndpointProvider) {
'use strict';
var service = {};
var user = {};
service.init = init;
service.OAuthLogin = OAuthLogin;
service.login = login;
service.logout = logout;
service.isAuthenticated = isAuthenticated;
@@ -15,30 +18,22 @@ angular.module('portainer.app')
var jwt = LocalStorage.getJWT();
if (jwt) {
var tokenPayload = jwtHelper.decodeToken(jwt);
user.username = tokenPayload.username;
user.ID = tokenPayload.id;
user.role = tokenPayload.role;
setUser(jwt);
}
}
function OAuthLogin(code) {
return OAuth.validate({ code: code }).$promise
.then(function onLoginSuccess(response) {
return setUser(response.jwt);
});
}
function login(username, password) {
var deferred = $q.defer();
Auth.login({username: username, password: password}).$promise
.then(function success(data) {
LocalStorage.storeJWT(data.jwt);
var tokenPayload = jwtHelper.decodeToken(data.jwt);
user.username = username;
user.ID = tokenPayload.id;
user.role = tokenPayload.role;
deferred.resolve();
})
.catch(function error() {
deferred.reject();
});
return deferred.promise;
return Auth.login({ username: username, password: password }).$promise
.then(function onLoginSuccess(response) {
return setUser(response.jwt);
});
}
function logout() {
@@ -56,5 +51,13 @@ angular.module('portainer.app')
return user;
}
function setUser(jwt) {
LocalStorage.storeJWT(jwt);
var tokenPayload = jwtHelper.decodeToken(jwt);
user.username = tokenPayload.username;
user.ID = tokenPayload.id;
user.role = tokenPayload.role;
}
return service;
}]);
+7 -1
View File
@@ -11,7 +11,13 @@ angular.module('portainer.app')
var codeMirrorYAMLOptions = {
mode: 'text/x-yaml',
gutters: ['CodeMirror-lint-markers'],
lint: true
lint: true,
extraKeys: {
Tab: function(cm) {
var spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
cm.replaceSelection(spaces);
}
}
};
service.applyCodeMirrorOnElement = function(element, yamlLint, readOnly) {
@@ -64,10 +64,6 @@ angular.module('portainer.app')
return endpoint.OfflineMode;
};
service.endpointStatusFromOfflineMode = function(isOffline) {
return isOffline ? 2 : 1;
};
service.setOfflineMode = function(isOffline) {
endpoint.OfflineMode = isOffline;
LocalStorage.storeOfflineMode(isOffline);
+11 -1
View File
@@ -5,6 +5,7 @@ angular.module('portainer.app')
var service = {};
var headers = {};
headers.agentTargetQueue = [];
headers.agentManagerOperation = false;
service.registryAuthenticationHeader = function() {
return headers.registryAuthentication;
@@ -36,9 +37,18 @@ angular.module('portainer.app')
}
};
service.resetAgentTargetQueue = function() {
service.setPortainerAgentManagerOperation = function(set) {
headers.agentManagerOperation = set;
};
service.portainerAgentManagerOperation = function() {
return headers.agentManagerOperation;
};
service.resetAgentHeaders = function() {
headers.agentTargetQueue = [];
delete headers.agentTargetLastValue;
headers.agentManagerOperation = false;
};
return service;
+4
View File
@@ -56,6 +56,10 @@
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
You cannot change your password when using LDAP authentication.
</span>
<span class="text-muted small" style="margin-left: 5px;" ng-if="AuthenticationMethod === 3 && userID !== 1">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
You cannot change your password when using OAuth authentication.
</span>
</div>
</div>
</form>
+27 -3
View File
@@ -9,7 +9,7 @@
</div>
<!-- !login box logo -->
<!-- login panel -->
<div class="panel panel-default">
<div class="panel panel-default" ng-show="!state.isInOAuthProcess">
<div class="panel-body">
<!-- login form -->
<form class="simple-box-form form-horizontal">
@@ -27,20 +27,44 @@
<!-- !password input -->
<!-- login button -->
<div class="form-group">
<div class="col-sm-12">
<div class="col-sm-12" >
<a ng-href="{{OAuthLoginURI}}" ng-if="AuthenticationMethod === 3">
<div class="btn btn-primary btn-sm pull-left" style="margin-left:2px" ng-if="state.OAuthProvider === 'Microsoft'">
<i class="fab fa-microsoft" aria-hidden="true"></i> Login with Microsoft
</div>
<div class="btn btn-primary btn-sm pull-left" style="margin-left:2px" ng-if="state.OAuthProvider === 'Google'">
<i class="fab fa-google" aria-hidden="true" ></i> Login with Google
</div>
<div class="btn btn-primary btn-sm pull-left" style="margin-left:2px" ng-if="state.OAuthProvider === 'Github'">
<i class="fab fa-github" aria-hidden="true" ></i> Login with Github
</div>
<div class="btn btn-primary btn-sm pull-left" style="margin-left:2px" ng-if="state.OAuthProvider === 'OAuth'">
<i class="fa fa-sign-in-alt" aria-hidden="true" ></i> Login with OAuth
</div>
</a>
<button type="submit" class="btn btn-primary btn-sm pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in-alt" aria-hidden="true"></i> Login</button>
<span class="pull-left" style="margin: 5px;" ng-if="state.AuthenticationError">
<span class="pull-right" style="margin: 5px;" ng-if="state.AuthenticationError">
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
<span class="small text-danger">{{ state.AuthenticationError }}</span>
</span>
</div>
</div>
<!-- !login button -->
</form>
<!-- !login form -->
</div>
</div>
<!-- !login panel -->
<div class="panel panel-default" ng-show="state.isInOAuthProcess">
<div class="panel-body">
<div class="form-group text-center">
<span class="small text-muted">OAuth authentication in progress... <span button-spinner="true"></span></span>
</div>
</div>
</div>
</div>
</div>
<!-- !login box -->
+47 -6
View File
@@ -1,7 +1,6 @@
angular.module('portainer.app')
.controller('AuthenticationController', ['$q', '$scope', '$state', '$transition$', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService',
function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService) {
.controller('AuthenticationController', ['$q', '$scope', '$state', '$stateParams', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', 'URLHelper',
function($q, $scope, $state, $stateParams, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService, URLHelper) {
$scope.logo = StateManager.getState().application.logo;
$scope.formValues = {
@@ -10,7 +9,9 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
};
$scope.state = {
AuthenticationError: ''
AuthenticationError: '',
isInOAuthProcess: true,
OAuthProvider: ''
};
$scope.authenticateUser = function() {
@@ -81,10 +82,31 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
});
}
function determineOauthProvider(LoginURI) {
if (LoginURI.indexOf('login.microsoftonline.com') !== -1) {
return 'Microsoft';
}
else if (LoginURI.indexOf('accounts.google.com') !== -1) {
return 'Google';
}
else if (LoginURI.indexOf('github.com') !== -1) {
return 'Github';
}
return 'OAuth';
}
function initView() {
if ($transition$.params().logout || $transition$.params().error) {
SettingsService.publicSettings()
.then(function success(settings) {
$scope.AuthenticationMethod = settings.AuthenticationMethod;
$scope.OAuthLoginURI = settings.OAuthLoginURI;
$scope.state.OAuthProvider = determineOauthProvider(settings.OAuthLoginURI);
});
if ($stateParams.logout || $stateParams.error) {
Authentication.logout();
$scope.state.AuthenticationError = $transition$.params().error;
$scope.state.AuthenticationError = $stateParams.error;
$scope.state.isInOAuthProcess = false;
return;
}
@@ -98,7 +120,26 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
} else {
authenticatedFlow();
}
var code = URLHelper.getParameter('code');
if (code) {
oAuthLogin(code);
} else {
$scope.state.isInOAuthProcess = false;
}
}
function oAuthLogin(code) {
return Authentication.OAuthLogin(code)
.then(function success() {
URLHelper.cleanParameters();
})
.catch(function error() {
$scope.state.AuthenticationError = 'Unable to login via OAuth';
$scope.state.isInOAuthProcess = false;
});
}
initView();
}]);
@@ -20,7 +20,7 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService,
};
$scope.copyAgentCommand = function() {
clipboard.copyText('curl -L https://portainer.io/download/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent');
clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent');
$('#copyNotification').show();
$('#copyNotification').fadeOut(2000);
};
@@ -67,7 +67,7 @@
Ensure that you have deployed the Portainer agent in your cluster first. You can use execute the following command on any manager node to deploy it.
<div style="margin-top: 10px;">
<code>
curl -L https://portainer.io/download/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent
curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent
</code>
<span class="btn btn-primary btn-sm space-left" ng-click="copyAgentCommand()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy</span>
<span>
+8 -35
View File
@@ -7,51 +7,24 @@
<span class="text-muted" style="font-size: 90%;">
<p>
Portainer CE is a great way of managing clusters, provisioning containers and services and
managing container environment lifecycles. To extend the benefit of Portainer CE even
more, and to address the needs of larger, complex or critical environments, the Portainer
managing container environment lifecycles. To extend the benefit of Portainer CE even more,
and to address the needs of larger, complex or critical environments, the Portainer
team provides a growing range of low-cost Extensions.
</p>
<p>
As the diagram shows, running a successful production container environment requires a
range of capability across a number of complex technical areas.
</p>
<p style="text-align: center; margin: 15px 0 15px 0;">
<img src="images/extensions_overview_diagram.png" alt="extensions overview">
To ensure that Portainer remains the best choice for managing production container platforms,
the Portainer team have chosen a modular, extensible design approach, where additional capability
can be added to the Portainer CE core as needed, and at very low cost.
</p>
<p>
Available through a simple subscription process from the menu, Portainer Extensions
answers this need and provides a simple way to enhance the functionality that Portainer
makes available through incremental capability in important areas.
Available through a simple subscription process from the list below, Portainer Extensions
provide a simple way to enhance Portainer CE’s core functionality through incremental capability in important areas.
</p>
<p>
The vision for Portainer is to be the standard management layer for container platforms. In
order to achieve this vision, Portainer CE will be extended across a range of new functional
areas. In order to ensure that Portainer remains the best choice for managing production
container platforms, the Portainer team have chosen a modular extensible design approach,
where additional capability can be added to the Portainer CE core as needed, and at very
low cost.
</p>
<p>
The advantage of an extensible design is clear: While a range of capability is available, only
necessary functionality is added as and when needed.
</p>
<p>
Our first extension is <a ui-sref="portainer.extensions.extension({id: 1})">Registry Manager</a>, available now. Others (such as
Single Sign On and Operations Management) are scheduled for the early part of 2019.
</p>
<p>
Portainer CE is the core of the Portainer management environments. Portainer CE will
continue to be developed and made freely available as part of our deep commitment to our
Open Source heritage and our user community. Portainer CE will always deliver great
functionality and remain the industry standard toolset for managing container-based
platforms.
For additional information on Portainer Extensions, see our website <a href="https://www.portainer.io/products-services/portainer-extension-software/" target="_blank">here</a>.
</p>
</span>
</information-panel>
@@ -36,7 +36,9 @@
<div class="form-group" style="margin-left: 40px;">
<div style="font-size: 125%; border-bottom: 2px solid #2d3e63; padding-bottom: 5px;">
{{ extension.Enabled ? 'Enabled' : extension.Price }}
<span ng-if="extension.Enabled">Enabled</span>
<span ng-if="!extension.Enabled && extension.License.Expiration && !extension.License.Valid">Expired</span>
<span ng-if="!extension.Enabled && !extension.License.Expiration">{{ extension.Price }}</span>
</div>
<div class="small text-muted col-sm-12" style="margin: 15px 0 15px 0;" ng-if="!extension.Enabled">
+1 -1
View File
@@ -9,7 +9,7 @@
<information-panel
ng-if="motd && motd.Message !== '' && applicationState.UI.dismissedInfoHash !== motd.Hash"
title-text="Important message"
title-text="{{ motd.Title }}"
dismiss-action="dismissImportantInformation(motd.Hash)">
<span class="text-muted">
<p ng-bind-html="motd.Message"></p>
+120 -120
View File
@@ -1,139 +1,139 @@
angular.module('portainer.app')
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'LegacyExtensionManager', 'ModalService', 'MotdService', 'SystemService',
function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, LegacyExtensionManager, ModalService, MotdService, SystemService) {
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'LegacyExtensionManager', 'ModalService', 'MotdService', 'SystemService',
function($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, LegacyExtensionManager, ModalService, MotdService, SystemService) {
$scope.goToEdit = function(id) {
$state.go('portainer.endpoints.endpoint', { id: id });
};
$scope.goToEdit = function(id) {
$state.go('portainer.endpoints.endpoint', { id: id });
};
$scope.goToDashboard = function (endpoint) {
if (endpoint.Type === 3) {
return switchToAzureEndpoint(endpoint);
}
$scope.goToDashboard = function(endpoint) {
if (endpoint.Type === 3) {
return switchToAzureEndpoint(endpoint);
}
checkEndpointStatus(endpoint)
.then(function sucess() {
return switchToDockerEndpoint(endpoint);
}).catch(function error(err) {
Notifications.error('Failure', err, 'Unable to verify endpoint status');
});
};
checkEndpointStatus(endpoint)
.then(function sucess() {
return switchToDockerEndpoint(endpoint);
}).catch(function error(err) {
Notifications.error('Failure', err, 'Unable to verify endpoint status');
});
};
$scope.dismissImportantInformation = function (hash) {
StateManager.dismissImportantInformation(hash);
};
$scope.dismissImportantInformation = function(hash) {
StateManager.dismissImportantInformation(hash);
};
$scope.dismissInformationPanel = function (id) {
StateManager.dismissInformationPanel(id);
};
$scope.dismissInformationPanel = function(id) {
StateManager.dismissInformationPanel(id);
};
$scope.triggerSnapshot = function () {
ModalService.confirmEndpointSnapshot(function (result) {
if (!result) {
return;
}
triggerSnapshot();
});
};
$scope.triggerSnapshot = function() {
ModalService.confirmEndpointSnapshot(function(result) {
if (!result) {
return;
}
triggerSnapshot();
});
};
function checkEndpointStatus(endpoint) {
var deferred = $q.defer();
function checkEndpointStatus(endpoint) {
var deferred = $q.defer();
var status = 1;
SystemService.ping(endpoint.Id)
.then(function sucess() {
status = 1;
}).catch(function error() {
status = 2;
}).finally(function() {
if (endpoint.Status === status) {
deferred.resolve(endpoint);
return deferred.promise;
}
EndpointService.updateEndpoint(endpoint.Id, { Status: status })
.then(function sucess() {
deferred.resolve(endpoint);
}).catch(function error(err) {
deferred.reject({ msg: 'Unable to update endpoint status', err: err });
});
});
var status = 1;
SystemService.ping(endpoint.Id)
.then(function sucess() {
status = 1;
}).catch(function error() {
status = 2;
}).finally(function () {
if (endpoint.Status === status) {
deferred.resolve(endpoint);
return deferred.promise;
}
EndpointService.updateEndpoint(endpoint.Id, { Status: status })
.then(function sucess() {
deferred.resolve(endpoint);
}).catch(function error(err) {
deferred.reject({msg: 'Unable to update endpoint status', err: err});
});
});
function switchToAzureEndpoint(endpoint) {
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
StateManager.updateEndpointState(endpoint, [])
.then(function success() {
$state.go('azure.dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint');
});
}
return deferred.promise;
}
function switchToDockerEndpoint(endpoint) {
if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) {
Notifications.error('Failure', '', 'Endpoint is unreachable. Connect to another swarm manager.');
return;
} else if (endpoint.Status === 2 && !endpoint.Snapshots[0]) {
Notifications.error('Failure', '', 'Endpoint is unreachable and there is no snapshot available for offline browsing.');
return;
}
function switchToAzureEndpoint(endpoint) {
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
StateManager.updateEndpointState(endpoint.Name, endpoint.Type, [])
.then(function success() {
$state.go('azure.dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint');
});
}
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
LegacyExtensionManager.initEndpointExtensions(endpoint)
.then(function success(data) {
var extensions = data;
return StateManager.updateEndpointState(endpoint, extensions);
})
.then(function success() {
$state.go('docker.dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
});
}
function switchToDockerEndpoint(endpoint) {
if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) {
Notifications.error('Failure', '', 'Endpoint is unreachable. Connect to another swarm manager.');
return;
} else if (endpoint.Status === 2 && !endpoint.Snapshots[0]) {
Notifications.error('Failure', '', 'Endpoint is unreachable and there is no snapshot available for offline browsing.');
return;
}
function triggerSnapshot() {
EndpointService.snapshotEndpoints()
.then(function success() {
Notifications.success('Success', 'Endpoints updated');
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'An error occured during endpoint snapshot');
});
}
EndpointProvider.setEndpointID(endpoint.Id);
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
EndpointProvider.setOfflineModeFromStatus(endpoint.Status);
LegacyExtensionManager.initEndpointExtensions(endpoint)
.then(function success(data) {
var extensions = data;
return StateManager.updateEndpointState(endpoint, extensions);
})
.then(function success() {
$state.go('docker.dashboard');
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
});
}
function initView() {
$scope.isAdmin = Authentication.getUserDetails().role === 1;
function triggerSnapshot() {
EndpointService.snapshotEndpoints()
.then(function success() {
Notifications.success('Success', 'Endpoints updated');
$state.reload();
})
.catch(function error(err) {
Notifications.error('Failure', err, 'An error occured during endpoint snapshot');
});
}
MotdService.motd()
.then(function success(data) {
$scope.motd = data;
});
function initView() {
$scope.isAdmin = Authentication.getUserDetails().role === 1;
$q.all({
endpoints: EndpointService.endpoints(),
groups: GroupService.groups()
})
.then(function success(data) {
var endpoints = data.endpoints;
var groups = data.groups;
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
$scope.endpoints = endpoints;
EndpointProvider.setEndpoints(endpoints);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
});
}
MotdService.motd()
.then(function success(data) {
$scope.motd = data;
});
$q.all({
endpoints: EndpointService.endpoints(),
groups: GroupService.groups()
})
.then(function success(data) {
var endpoints = data.endpoints;
var groups = data.groups;
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
$scope.endpoints = endpoints;
EndpointProvider.setEndpoints(endpoints);
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
});
}
initView();
}]);
initView();
}]);
@@ -7,7 +7,7 @@
<rd-header-content>Registry management</rd-header-content>
</rd-header>
<div class="row" ng-if="dockerhub">
<div class="row" ng-if="dockerhub && isAdmin">
<div class="col-sm-12">
<rd-widget>
<rd-widget-header icon="fa-database" title-text="DockerHub">
@@ -74,7 +74,7 @@
title-text="Registries" title-icon="fa-database"
dataset="registries" table-key="registries"
order-by="Name"
access-management="applicationState.application.authentication"
access-management="applicationState.application.authentication && isAdmin"
remove-action="removeAction"
registry-management="registryManagementAvailable"
></registries-datatable>
@@ -1,6 +1,6 @@
angular.module('portainer.app')
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'ExtensionService',
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService) {
.controller('RegistriesController', ['$q', '$scope', '$state', 'RegistryService', 'DockerHubService', 'ModalService', 'Notifications', 'ExtensionService', 'Authentication',
function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, Notifications, ExtensionService, Authentication) {
$scope.state = {
actionInProgress: false
@@ -67,6 +67,12 @@ function ($q, $scope, $state, RegistryService, DockerHubService, ModalService, N
$scope.registries = data.registries;
$scope.dockerhub = data.dockerhub;
$scope.registryManagementAvailable = data.registryManagement;
var authenticationEnabled = $scope.applicationState.application.authentication;
if (authenticationEnabled) {
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1;
$scope.isAdmin = isAdmin;
}
})
.catch(function error(err) {
$scope.registries = [];
@@ -37,23 +37,49 @@
<p>LDAP authentication</p>
</label>
</div>
<div ng-if="oauthAuthenticationAvailable">
<input type="radio" id="registry_auth" ng-model="settings.AuthenticationMethod" ng-value="3">
<label for="registry_auth">
<div class="boxselector_header">
<i class="fa fa-users" aria-hidden="true" style="margin-right: 2px;"></i>
OAuth
</div>
<p>OAuth authentication</p>
</label>
</div>
<div style="color: #767676;" ng-click="goToOAuthExtensionView()" ng-if="!oauthAuthenticationAvailable">
<input type="radio" id="registry_auth" ng-model="settings.AuthenticationMethod" ng-value="3" disabled>
<label for="registry_auth" tooltip-append-to-body="true" tooltip-placement="bottom" tooltip-class="portainer-tooltip" uib-tooltip="Feature available via an extension" style="cursor:pointer; border-color: #767676">
<div class="boxselector_header">
<i class="fa fa-users" aria-hidden="true" style="margin-right: 2px;"></i>
OAuth (extension)
</div>
<p>OAuth authentication</p>
</label>
</div>
</div>
</div>
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group" ng-if="settings.AuthenticationMethod === 1">
<span class="col-sm-12 text-muted small">
<div ng-if="settings.AuthenticationMethod === 1">
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group col-sm-12 text-muted small">
When using internal authentication, Portainer will encrypt user passwords and store credentials locally.
</span>
</div>
<div class="form-group" ng-if="settings.AuthenticationMethod === 2">
<span class="col-sm-12 text-muted small">
When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails.
</span>
</div>
</div>
<div ng-if="settings.AuthenticationMethod === 2">
<div>
<div class="col-sm-12 form-section-title">
Information
</div>
<div class="form-group col-sm-12 text-muted small">
When using LDAP authentication, Portainer will delegate user authentication to a LDAP server and fallback to internal authentication if LDAP authentication fails.
</div>
</div>
<div class="col-sm-12 form-section-title">
LDAP configuration
</div>
@@ -306,7 +332,12 @@
<!-- !group-search-settings -->
</div>
<!-- actions -->
<oauth-settings ng-if="isOauthEnabled()" settings="OAuthSettings" teams="teams"></oauth-settings>
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
</div>
<div class="form-group">
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()" ng-disabled="state.actionInProgress" button-spinner="state.actionInProgress">
@@ -1,6 +1,6 @@
angular.module('portainer.app')
.controller('SettingsAuthenticationController', ['$q', '$scope', 'Notifications', 'SettingsService', 'FileUploadService',
function ($q, $scope, Notifications, SettingsService, FileUploadService) {
.controller('SettingsAuthenticationController', ['$q', '$scope', '$state', 'Notifications', 'SettingsService', 'FileUploadService', 'TeamService', 'ExtensionService',
function($q, $scope, $state, Notifications, SettingsService, FileUploadService, TeamService, ExtensionService) {
$scope.state = {
successfulConnectivityCheck: false,
@@ -14,6 +14,14 @@ function ($q, $scope, Notifications, SettingsService, FileUploadService) {
TLSCACert: ''
};
$scope.goToOAuthExtensionView = function() {
$state.go('portainer.extensions.extension', { id: 2 });
};
$scope.isOauthEnabled = function isOauthEnabled() {
return $scope.settings && $scope.settings.AuthenticationMethod === 3;
};
$scope.addSearchConfiguration = function() {
$scope.LDAPSettings.SearchSettings.push({ BaseDN: '', UserNameAttribute: '', Filter: '' });
};
@@ -21,7 +29,7 @@ function ($q, $scope, Notifications, SettingsService, FileUploadService) {
$scope.removeSearchConfiguration = function(index) {
$scope.LDAPSettings.SearchSettings.splice(index, 1);
};
$scope.addGroupSearchConfiguration = function() {
$scope.LDAPSettings.GroupSearchSettings.push({ GroupBaseDN: '', GroupAttribute: '', GroupFilter: '' });
};
@@ -92,12 +100,19 @@ function ($q, $scope, Notifications, SettingsService, FileUploadService) {
}
function initView() {
SettingsService.settings()
$q.all({
settings: SettingsService.settings(),
teams: TeamService.teams(),
oauthAuthentication: ExtensionService.OAuthAuthenticationEnabled()
})
.then(function success(data) {
var settings = data;
var settings = data.settings;
$scope.teams = data.teams;
$scope.settings = settings;
$scope.LDAPSettings = settings.LDAPSettings;
$scope.OAuthSettings = settings.OAuthSettings;
$scope.formValues.TLSCACert = settings.LDAPSettings.TLSConfig.TLSCACert;
$scope.oauthAuthenticationAvailable = data.oauthAuthentication;
})
.catch(function error(err) {
Notifications.error('Failure', err, 'Unable to retrieve application settings');
+1 -1
View File
@@ -63,7 +63,7 @@
<a ui-sref="portainer.tags" ui-sref-active="active">Tags</a>
</div>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
<li class="sidebar-list" ng-if="applicationState.application.authentication">
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
</li>
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
+3 -3
View File
@@ -1,6 +1,6 @@
angular.module('portainer.app')
.controller('TeamsController', ['$q', '$scope', '$state', '$sanitize', 'TeamService', 'UserService', 'ModalService', 'Notifications', 'Authentication',
function ($q, $scope, $state, $sanitize, TeamService, UserService, ModalService, Notifications, Authentication) {
.controller('TeamsController', ['$q', '$scope', '$state', 'TeamService', 'UserService', 'ModalService', 'Notifications', 'Authentication',
function ($q, $scope, $state, TeamService, UserService, ModalService, Notifications, Authentication) {
$scope.state = {
actionInProgress: false
};
@@ -22,7 +22,7 @@ function ($q, $scope, $state, $sanitize, TeamService, UserService, ModalService,
};
$scope.addTeam = function() {
var teamName = $sanitize($scope.formValues.Name);
var teamName = $scope.formValues.Name;
var leaderIds = [];
angular.forEach($scope.formValues.Leaders, function(user) {
leaderIds.push(user.Id);
-44
View File
@@ -1,44 +0,0 @@
version: 1.0.{build}
image:
- Visual Studio 2017
- Ubuntu
environment:
matrix:
- ARCH: amd64
- ARCH: arm
- ARCH: arm64
- ARCH: ppc64le
- ARCH: s390x
DOCKER_USER:
secure: JapmC7j5F0mY3j/MVzU+Cw==
DOCKER_PASS:
secure: QGlCLNWzPD0HL8ipkohVic45/yU3bVOdjn0IiV6NnSQ=
matrix:
exclude:
- image: Visual Studio 2017
ARCH: arm
- image: Visual Studio 2017
ARCH: arm64
- image: Visual Studio 2017
ARCH: ppc64le
- image: Visual Studio 2017
ARCH: s390x
branches:
except:
- master
stack:
- node 9, go 1.10
install:
- yarn install
- npm install -g rebase-docker-image
init:
- sh: export IMAGE=linux
- cmd: SET IMAGE=windows
- ps: >-
if (!(Test-Path ~/.docker)) { mkdir ~/.docker };
Set-Content -Value '{ "experimental": "enabled" }' -Path ~/.docker/config.json -Encoding Ascii
build_script:
- sh: yarn grunt appveyorbuild:$IMAGE:$ARCH
- cmd: yarn grunt appveyorbuild:%IMAGE%:%ARCH%
- sh: sudo bash build/ci-linux.sh $IMAGE $ARCH $DOCKER_USER $DOCKER_PASS $APPVEYOR_REPO_BRANCH $APPVEYOR_PULL_REQUEST_NUMBER
- cmd: powershell -Command "& .\\build\\ci-windows.ps1"
-61
View File
@@ -1,61 +0,0 @@
version: 1.0.{build}
image:
- Visual Studio 2017
- Ubuntu
environment:
matrix:
- ARCH: amd64
- ARCH: arm
- ARCH: arm64
- ARCH: ppc64le
- ARCH: s390x
DOCKER_USER:
secure: JapmC7j5F0mY3j/MVzU+Cw==
DOCKER_PASS:
secure: QGlCLNWzPD0HL8ipkohVic45/yU3bVOdjn0IiV6NnSQ=
PORTAINER_VERSION: "1.19.2"
matrix:
exclude:
- image: Visual Studio 2017
ARCH: arm
- image: Visual Studio 2017
ARCH: arm64
- image: Visual Studio 2017
ARCH: ppc64le
- image: Visual Studio 2017
ARCH: s390x
branches:
only:
- master
stack: node 9, go 1.10
artifacts:
- path: 'portainer-$(PORTAINER_VERSION)-$(IMAGE)-$(ARCH).tar.gz'
type: file
- path: 'portainer-$(PORTAINER_VERSION)-$(IMAGE)-$(ARCH)-checksum.txt'
type: file
install:
- yarn install
- npm install -g rebase-docker-image
init:
- sh: export IMAGE=linux
- cmd: SET IMAGE=windows
- ps: >-
if (!(Test-Path ~/.docker)) { mkdir ~/.docker }
Set-Content -Value '{ "experimental": "enabled" }' -Path ~/.docker/config.json -Encoding Ascii
build_script:
- sh: yarn grunt appveyorbuild:$IMAGE:$ARCH
- cmd: yarn grunt appveyorbuild:%IMAGE%:%ARCH%
- sh: sudo bash build/release-linux.sh $IMAGE $ARCH $PORTAINER_VERSION $DOCKER_USER $DOCKER_PASS
- cmd: powershell -Command "& .\\build\\release-windows.ps1"
test: off
deploy:
release: Release $(PORTAINER_VERSION)
description: ''
provider: GitHub
auth_token:
secure: BRYVGj94QlFBCMoO8yhSu+AGqKNV1+03LJEFrNUTRzo5erXfUHUIi/rgztnxfSGW
artifact: /portainer-$(PORTAINER_VERSION)-$(IMAGE)-$(ARCH).*/
draft: true
prerelease: false
on:
branch: master
+17
View File
@@ -0,0 +1,17 @@
param (
[string]$platform,
[string]$arch
)
$ErrorActionPreference = "Stop";
$binary = "portainer.exe"
$project_path = (Get-ITEM -Path env:APPVEYOR_BUILD_FOLDER).Value
New-Item -Name dist -Path "$project_path" -ItemType Directory | Out-Null
Set-Location -Path "$project_path\api\cmd\portainer"
C:\go\bin\go.exe get -t -d -v ./...
C:\go\bin\go.exe build -v
Move-Item -Path "$($binary)" -Destination "..\..\..\dist"
+9
View File
@@ -0,0 +1,9 @@
binary="portainer"
mkdir -p dist
cd 'api/cmd/portainer'
go get -t -d -v ./...
GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s'
mv "${binary}" "../../../dist/portainer"
+24
View File
@@ -0,0 +1,24 @@
param (
[string]$platform,
[string]$arch
)
$ErrorActionPreference = "Stop";
$binary = "portainer.exe"
$project_path = (Get-ITEM -Path env:BUILD_SOURCESDIRECTORY).Value
Set-Item env:GOPATH "$project_path\api"
New-Item -Name dist -Path "$project_path" -ItemType Directory | Out-Null
New-Item -Name portainer -Path "$project_path\api\src\github.com\" -ItemType Directory | Out-Null
Copy-Item -Path "$project_path\api" -Destination "$project_path\api\src\github.com\portainer" -Recurse -Force -ErrorAction:SilentlyContinue
Rename-Item -Path "$project_path\api\src\github.com\portainer\api" -NewName "portainer" -ErrorAction:SilentlyContinue
Set-Location -Path "$project_path\api\cmd\portainer"
go.exe get -t -d -v ./...
go.exe build -v
Move-Item -Path "$project_path\api\cmd\portainer\$($binary)" -Destination "$project_path\dist"
+15
View File
@@ -0,0 +1,15 @@
export GOPATH="$BUILD_SOURCESDIRECTORY/api"
binary="portainer"
mkdir -p dist
mkdir -p api/src/github.com/portainer/
cp -R api/ api/src/github.com/portainer/portainer/
cd 'api/cmd/portainer'
go get -t -d -v ./...
GOOS=$1 GOARCH=$2 CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s'
mv "$BUILD_SOURCESDIRECTORY/api/cmd/portainer/$binary" "$BUILD_SOURCESDIRECTORY/dist/portainer"
+13
View File
@@ -0,0 +1,13 @@
param (
[string]$docker_version
)
$ErrorActionPreference = "Stop";
New-Item -Path "docker-binary" -ItemType Directory | Out-Null
$download_folder = "docker-binary"
Invoke-WebRequest -O "$($download_folder)/docker-binaries.zip" "https://download.docker.com/win/static/stable/x86_64/docker-$($docker_version).zip"
Expand-Archive -Path "$($download_folder)/docker-binaries.zip" -DestinationPath "$($download_folder)"
Move-Item -Path "$($download_folder)/docker/docker.exe" -Destination "dist"

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