Compare commits

...

93 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
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
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
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 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
70 changed files with 1181 additions and 267 deletions
+1 -3
View File
@@ -1,6 +1,6 @@
<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/)
@@ -8,7 +8,6 @@
[![Documentation Status](https://readthedocs.org/projects/portainer/badge/?version=stable)](http://portainer.readthedocs.io/en/stable/?badge=stable)
[![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)
[![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).
@@ -41,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
+4 -3
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,
+2 -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
+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)
+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 != "" {
+12 -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")
@@ -74,6 +75,15 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
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 {
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
}
+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
+23 -2
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"`
@@ -779,15 +794,17 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "1.20.1"
APIVersion = "1.20.2"
// DBVersion is the version number of the Portainer database
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
@@ -834,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 (
@@ -912,6 +931,8 @@ const (
_ ExtensionID = iota
// RegistryManagementExtension represents the registry management extension
RegistryManagementExtension
// OAuthAuthenticationExtension represents the OAuth authentication extension
OAuthAuthenticationExtension
)
const (
+2 -2
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.1"
version: "1.20.2"
title: "Portainer API"
contact:
email: "info@portainer.io"
@@ -3018,7 +3018,7 @@ definitions:
description: "Is analytics enabled"
Version:
type: "string"
example: "1.20.1"
example: "1.20.2"
description: "Portainer API version"
PublicSettingsInspectResponse:
type: "object"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"packageName": "portainer",
"packageVersion": "1.20.1",
"packageVersion": "1.20.2",
"projectName": "portainer"
}
+1 -1
View File
@@ -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', redirect: $state.current.name});
$state.go('portainer.auth', { error: 'Your session has expired' });
}
});
}
+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']);
@@ -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>
+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: '',
@@ -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">
+1 -1
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>
@@ -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) {
@@ -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'
}
}
});
}]);
@@ -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;
+12 -2
View File
@@ -48,7 +48,7 @@ angular.module('portainer.app', [])
var authentication = {
name: 'portainer.auth',
url: '/auth?redirect',
url: '/auth',
params: {
logout: false,
error: ''
@@ -329,7 +329,7 @@ angular.module('portainer.app', [])
}
},
resolve: {
endpointID: ['EndpointProvider', '$state',
endpointID: ['EndpointProvider', '$state',
function (EndpointProvider, $state) {
var id = EndpointProvider.endpointID();
if (!id) {
@@ -457,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>
@@ -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">
+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
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) {
+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 -->
+49 -8
View File
@@ -1,7 +1,6 @@
angular.module('portainer.app')
.controller('AuthenticationController', ['$q', '$scope', '$state', '$transition$', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService', '$stateParams',
function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService, $stateParams) {
.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() {
@@ -44,7 +45,7 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
if (endpoints.length === 0) {
$state.go('portainer.init.endpoint');
} else {
$state.go($stateParams.redirect ||'portainer.home');
$state.go('portainer.home');
}
})
.catch(function error(err) {
@@ -73,7 +74,7 @@ function ($q, $scope, $state, $transition$, $sanitize, Authentication, UserServi
if (endpoints.length === 0 && userDetails.role === 1) {
$state.go('portainer.init.endpoint');
} else {
$state.go($stateParams.redirect || 'portainer.home');
$state.go('portainer.home');
}
})
.catch(function error(err) {
@@ -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);
};
+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>
+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">
+1 -1
View File
@@ -1,5 +1,5 @@
Name: portainer
Version: 1.20.1
Version: 1.20.2
Release: 0
License: Zlib
Summary: A lightweight docker management UI
+5 -5
View File
@@ -56,7 +56,7 @@ module.exports = function(grunt) {
});
grunt.registerTask('lint', ['eslint']);
grunt.registerTask('run-dev', ['build', 'shell:run:' + arch, 'watch:build']);
grunt.registerTask('run-dev', ['build', 'shell:run', 'watch:build']);
grunt.registerTask('clear', ['clean:app']);
// Load content of `vendor.yml` to src.jsVendor, src.cssVendor and src.angularVendor
@@ -85,7 +85,7 @@ module.exports = function(grunt) {
grunt.initConfig({
root: 'dist',
distdir: 'dist/public',
shippedDockerVersion: '18.09.0',
shippedDockerVersion: '18.09.3',
shippedDockerVersionWindows: '17.09.0-ce',
pkg: grunt.file.readJSON('package.json'),
config: gruntfile_cfg.config,
@@ -264,7 +264,7 @@ gruntfile_cfg.replace = {
};
function shell_buildBinary(p, a) {
var binfile = 'dist/portainer-' + p + '-' + a;
var binfile = 'dist/portainer';
if (p === 'linux') {
return [
'if [ -f ' + (binfile) + ' ]; then',
@@ -292,10 +292,10 @@ function shell_buildBinaryOnDevOps(p, a) {
}
}
function shell_run(arch) {
function shell_run() {
return [
'docker rm -f portainer',
'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer-linux-' + arch + ' --no-analytics --template-file /app/templates.json'
'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics --template-file /app/templates.json'
].join(';');
}
+6 -7
View File
@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "1.20.1",
"version": "1.20.2",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"
@@ -20,12 +20,11 @@
}
],
"scripts": {
"grunt": "grunt",
"start": "grunt run-dev",
"start:server": "yarn build:server:offline && grunt shell:run:amd64",
"start:server": "yarn build:server:offline && grunt shell:run",
"clean:all": "grunt clean:all",
"build": "grunt build",
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer-linux-amd64"
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer"
},
"engines": {
"node": ">= 0.8.4"
@@ -37,7 +36,7 @@
"angular-clipboard": "^1.6.2",
"angular-cookies": "~1.5.0",
"angular-file-saver": "^1.1.3",
"angular-google-analytics": "github:revolunet/angular-google-analytics#~1.1.9",
"angular-google-analytics": "github:revolunet/angular-google-analytics#semver:~1.1.9",
"angular-json-tree": "1.0.1",
"angular-jwt": "~0.1.8",
"angular-loading-bar": "~0.9.0",
@@ -63,8 +62,8 @@
"moment": "^2.21.0",
"ng-file-upload": "~12.2.13",
"rdash-ui": "1.0.*",
"splitargs": "github:deviantony/splitargs#~0.2.0",
"toastr": "github:CodeSeven/toastr#~2.1.3",
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
"toastr": "github:CodeSeven/toastr#semver:~2.1.3",
"ui-select": "^0.19.8",
"xterm": "^3.8.0"
},