repo tag delete

This commit is contained in:
Wenkai Yin 2016-04-15 13:17:32 +08:00
parent fefcae2634
commit 85869f1b81
14 changed files with 326 additions and 107 deletions

View File

@ -57,7 +57,10 @@ func (b *BaseAPI) ValidateUser() int {
username, password, ok := b.Ctx.Request.BasicAuth()
if ok {
log.Infof("Requst with Basic Authentication header, username: %s", username)
user, err := auth.Login(models.AuthModel{username, password})
user, err := auth.Login(models.AuthModel{
Principal: username,
Password: password,
})
if err != nil {
log.Errorf("Error while trying to login, username: %s, error: %v", username, err)
user = nil

View File

@ -18,6 +18,7 @@ package api
import (
"encoding/json"
"net/http"
"os"
"strconv"
"strings"
"time"
@ -26,6 +27,8 @@ import (
"github.com/vmware/harbor/models"
svc_utils "github.com/vmware/harbor/service/utils"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/vmware/harbor/utils/registry/auth"
)
// RepositoryAPI handles request to /api/repositories /api/repositories/tags /api/repositories/manifests, the parm has to be put
@ -36,6 +39,7 @@ type RepositoryAPI struct {
BaseAPI
userID int
username string
registry *registry.Registry
}
// Prepare will set a non existent user ID in case the request tries to view repositories under a project he doesn't has permission.
@ -53,6 +57,46 @@ func (ra *RepositoryAPI) Prepare() {
} else {
ra.username = username
}
var client *http.Client
//no session, initialize a standard auth handler
if ra.userID == dao.NonExistUserID && len(ra.username) == 0 {
credential := &auth.Credential{}
username, password, ok := ra.Ctx.Request.BasicAuth()
if ok {
credential.Username = username
credential.Password = password
}
client = registry.NewClientStandardAuthHandlerEmbeded(credential)
log.Debug("initializing standard auth handler")
} else {
// session works, initialize a username auth handler
username := ra.username
if len(username) == 0 {
user, err := dao.GetUser(models.User{
UserID: ra.userID,
})
if err != nil {
log.Errorf("error occurred whiling geting user for initializing a username auth handler: %v", err)
return
}
username = user.Username
}
client = registry.NewClientUsernameAuthHandlerEmbeded(username)
log.Debug("initializing username auth handler: %s", username)
}
endpoint := os.Getenv("REGISTRY_URL")
r, err := registry.New(endpoint, client)
if err != nil {
log.Fatalf("error occurred while initializing auth handler for repository API: %v", err)
}
ra.registry = r
}
// Get ...
@ -77,11 +121,13 @@ func (ra *RepositoryAPI) Get() {
ra.RenderError(http.StatusForbidden, "")
return
}
repoList, err := svc_utils.GetRepoFromCache()
if err != nil {
log.Errorf("Failed to get repo from cache, error: %v", err)
ra.RenderError(http.StatusInternalServerError, "internal sever error")
}
projectName := p.Name
q := ra.GetString("q")
var resp []string
@ -105,6 +151,56 @@ func (ra *RepositoryAPI) Get() {
ra.ServeJSON()
}
// Delete ...
func (ra *RepositoryAPI) Delete() {
repoName := ra.GetString("repo_name")
if len(repoName) == 0 {
ra.CustomAbort(http.StatusBadRequest, "repo_name is nil")
}
tags := []string{}
tag := ra.GetString("tag")
if len(tag) == 0 {
tagList, err := ra.registry.ListTag(repoName)
if err != nil {
e, ok := registry.ParseError(err)
if ok {
log.Info(e)
ra.CustomAbort(e.StatusCode, e.Message)
} else {
log.Error(err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
}
tags = append(tags, tagList...)
} else {
tags = append(tags, tag)
}
for _, t := range tags {
if err := ra.registry.DeleteTag(repoName, t); err != nil {
e, ok := registry.ParseError(err)
if ok {
ra.CustomAbort(e.StatusCode, e.Message)
} else {
log.Error(err)
ra.CustomAbort(http.StatusInternalServerError, "internal error")
}
}
log.Infof("delete tag: %s %s", repoName, t)
}
go func() {
log.Debug("refreshing catalog cache")
if err := svc_utils.RefreshCatalogCache(); err != nil {
log.Errorf("error occurred while refresh catalog cache: %v", err)
}
}()
}
type tag struct {
Name string `json:"name"`
Tags []string `json:"tags"`
@ -128,7 +224,7 @@ func (ra *RepositoryAPI) GetTags() {
var tags []string
repoName := ra.GetString("repo_name")
result, err := svc_utils.RegistryAPIGet(svc_utils.BuildRegistryURL(repoName, "tags", "list"), ra.username)
result, err := registry.APIGet(registry.BuildRegistryURL(repoName, "tags", "list"), ra.username)
if err != nil {
log.Errorf("Failed to get repo tags, repo name: %s, error: %v", repoName, err)
ra.RenderError(http.StatusInternalServerError, "Failed to get repo tags")
@ -148,7 +244,7 @@ func (ra *RepositoryAPI) GetManifests() {
item := models.RepoItem{}
result, err := svc_utils.RegistryAPIGet(svc_utils.BuildRegistryURL(repoName, "manifests", tag), ra.username)
result, err := registry.APIGet(registry.BuildRegistryURL(repoName, "manifests", tag), ra.username)
if err != nil {
log.Errorf("Failed to get manifests for repo, repo name: %s, tag: %s, error: %v", repoName, tag, err)
ra.RenderError(http.StatusInternalServerError, "Internal Server Error")

View File

@ -49,7 +49,10 @@ func (c *CommonController) Login() {
principal := c.GetString("principal")
password := c.GetString("password")
user, err := auth.Login(models.AuthModel{principal, password})
user, err := auth.Login(models.AuthModel{
Principal: principal,
Password: password,
})
if err != nil {
log.Errorf("Error occurred in UserLogin: %v", err)
c.CustomAbort(http.StatusUnauthorized, "")

View File

@ -206,7 +206,10 @@ func TestLoginByUserName(t *testing.T) {
Password: "Abc12345",
}
loginUser, err := LoginByDb(models.AuthModel{userQuery.Username, userQuery.Password})
loginUser, err := LoginByDb(models.AuthModel{
Principal: userQuery.Username,
Password: userQuery.Password,
})
if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err)
}
@ -226,7 +229,10 @@ func TestLoginByEmail(t *testing.T) {
Password: "Abc12345",
}
loginUser, err := LoginByDb(models.AuthModel{userQuery.Email, userQuery.Password})
loginUser, err := LoginByDb(models.AuthModel{
Principal: userQuery.Email,
Password: userQuery.Password,
})
if err != nil {
t.Errorf("Error occurred in LoginByDb: %v", err)
}

View File

@ -38,7 +38,7 @@ const manifestPattern = `^application/vnd.docker.distribution.manifest.v\d\+json
// Post handles POST request, and records audit log or refreshes cache based on event.
func (n *NotificationHandler) Post() {
var notification models.Notification
// log.Info("Notification Handler triggered!\n")
//log.Info("Notification Handler triggered!\n")
// log.Infof("request body in string: %s", string(n.Ctx.Input.CopyBody()))
err := json.Unmarshal(n.Ctx.Input.CopyBody(1<<32), &notification)

View File

@ -13,7 +13,7 @@
limitations under the License.
*/
package utils
package token
import (
"crypto"
@ -80,7 +80,7 @@ func FilterAccess(username string, authenticated bool, a *token.ResourceActions)
return
}
if exist {
permission = "RW"
permission = "RWM"
} else {
permission = ""
log.Infof("project %s does not exist, set empty permission for admin\n", projectName)
@ -96,6 +96,9 @@ func FilterAccess(username string, authenticated bool, a *token.ResourceActions)
if strings.Contains(permission, "W") {
a.Actions = append(a.Actions, "push")
}
if strings.Contains(permission, "M") {
a.Actions = append(a.Actions, "*")
}
if strings.Contains(permission, "R") || dao.IsProjectPublic(projectName) {
a.Actions = append(a.Actions, "pull")
}

View File

@ -13,53 +13,53 @@
limitations under the License.
*/
package service
package token
import (
"net/http"
"github.com/vmware/harbor/auth"
"github.com/vmware/harbor/models"
svc_utils "github.com/vmware/harbor/service/utils"
//svc_utils "github.com/vmware/harbor/service/utils"
"github.com/vmware/harbor/utils/log"
"github.com/astaxie/beego"
"github.com/docker/distribution/registry/auth/token"
)
// TokenHandler handles request on /service/token, which is the auth provider for registry.
type TokenHandler struct {
// Handler handles request on /service/token, which is the auth provider for registry.
type Handler struct {
beego.Controller
}
// Get handles GET request, it checks the http header for user credentials
// and parse service and scope based on docker registry v2 standard,
// checkes the permission agains local DB and generates jwt token.
func (a *TokenHandler) Get() {
func (h *Handler) Get() {
request := a.Ctx.Request
request := h.Ctx.Request
log.Infof("request url: %v", request.URL.String())
username, password, _ := request.BasicAuth()
authenticated := authenticate(username, password)
service := a.GetString("service")
scopes := a.GetStrings("scope")
service := h.GetString("service")
scopes := h.GetStrings("scope")
log.Debugf("scopes: %+v", scopes)
if len(scopes) == 0 && !authenticated {
log.Info("login request with invalid credentials")
a.CustomAbort(http.StatusUnauthorized, "")
h.CustomAbort(http.StatusUnauthorized, "")
}
access := svc_utils.GetResourceActions(scopes)
access := GetResourceActions(scopes)
for _, a := range access {
svc_utils.FilterAccess(username, authenticated, a)
FilterAccess(username, authenticated, a)
}
a.serveToken(username, service, access)
h.serveToken(username, service, access)
}
func (a *TokenHandler) serveToken(username, service string, access []*token.ResourceActions) {
writer := a.Ctx.ResponseWriter
func (h *Handler) serveToken(username, service string, access []*token.ResourceActions) {
writer := h.Ctx.ResponseWriter
//create token
rawToken, err := svc_utils.MakeToken(username, service, access)
rawToken, err := MakeToken(username, service, access)
if err != nil {
log.Errorf("Failed to make token, error: %v", err)
writer.WriteHeader(http.StatusInternalServerError)
@ -67,12 +67,15 @@ func (a *TokenHandler) serveToken(username, service string, access []*token.Reso
}
tk := make(map[string]string)
tk["token"] = rawToken
a.Data["json"] = tk
a.ServeJSON()
h.Data["json"] = tk
h.ServeJSON()
}
func authenticate(principal, password string) bool {
user, err := auth.Login(models.AuthModel{principal, password})
user, err := auth.Login(models.AuthModel{
Principal: principal,
Password: password,
})
if err != nil {
log.Errorf("Error occurred in UserLogin: %v", err)
return false

View File

@ -17,10 +17,12 @@ package utils
import (
"encoding/json"
"os"
"time"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/utils/log"
"github.com/vmware/harbor/utils/registry"
"github.com/astaxie/beego/cache"
)
@ -28,6 +30,8 @@ import (
// Cache is the global cache in system.
var Cache cache.Cache
var registryClient *registry.Registry
const catalogKey string = "catalog"
func init() {
@ -36,11 +40,19 @@ func init() {
if err != nil {
log.Errorf("Failed to initialize cache, error:%v", err)
}
endpoint := os.Getenv("REGISTRY_URL")
client := registry.NewClientUsernameAuthHandlerEmbeded("admin")
registryClient, err = registry.New(endpoint, client)
if err != nil {
log.Fatalf("error occurred while initializing authentication handler used by cache: %v", err)
}
}
// RefreshCatalogCache calls registry's API to get repository list and write it to cache.
func RefreshCatalogCache() error {
result, err := RegistryAPIGet(BuildRegistryURL("_catalog"), "")
log.Debug("refreshing catalog cache...")
result, err := registry.APIGet(registry.BuildRegistryURL("_catalog"), "")
if err != nil {
return err
}
@ -49,7 +61,23 @@ func RefreshCatalogCache() error {
if err != nil {
return err
}
Cache.Put(catalogKey, repoResp.Repositories, 600*time.Second)
repos := []string{}
for _, repo := range repoResp.Repositories {
tags, err := registryClient.ListTag(repo)
if err != nil {
log.Errorf("error occurred while list tag for %s: %v", repo, err)
return err
}
if len(tags) != 0 {
repos = append(repos, repo)
log.Debugf("add %s to catalog cache", repo)
}
}
Cache.Put(catalogKey, repos, 600*time.Second)
return nil
}

View File

@ -19,6 +19,7 @@ import (
"github.com/vmware/harbor/api"
"github.com/vmware/harbor/controllers"
"github.com/vmware/harbor/service"
"github.com/vmware/harbor/service/token"
"github.com/astaxie/beego"
)
@ -63,5 +64,5 @@ func initRouters() {
//external service that hosted on harbor process:
beego.Router("/service/notifications", &service.NotificationHandler{})
beego.Router("/service/token", &service.TokenHandler{})
beego.Router("/service/token", &token.Handler{})
}

View File

@ -23,10 +23,7 @@ import (
"net/url"
"strings"
"github.com/astaxie/beego"
"github.com/vmware/harbor/dao"
"github.com/vmware/harbor/models"
"github.com/vmware/harbor/service/utils"
token_util "github.com/vmware/harbor/service/token"
"github.com/vmware/harbor/utils/log"
)
@ -57,7 +54,9 @@ type standardTokenHandler struct {
credential *Credential
}
// NewTokenHandler ...
// NewStandardTokenHandler returns a standard token handler. The handler will request a token
// from token server whose URL is specified in the "WWW-authentication" header and add it to
// the origin request
// TODO deal with https
func NewStandardTokenHandler(credential *Credential) Handler {
return &standardTokenHandler{
@ -68,12 +67,12 @@ func NewStandardTokenHandler(credential *Credential) Handler {
}
}
// Scheme : see interface AuthHandler
// Schema implements the corresponding method in interface AuthHandler
func (t *standardTokenHandler) Schema() string {
return "bearer"
}
// AuthorizeRequest : see interface AuthHandler
// AuthorizeRequest implements the corresponding method in interface AuthHandler
func (t *standardTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
realm, ok := params["realm"]
if !ok {
@ -103,9 +102,7 @@ func (t *standardTokenHandler) AuthorizeRequest(req *http.Request, params map[st
}
// TODO support secretKey
if len(t.credential.Username) != 0 {
r.SetBasicAuth(t.credential.Username, t.credential.Password)
}
r.SetBasicAuth(t.credential.Username, t.credential.Password)
resp, err := t.client.Do(r)
if err != nil {
@ -133,52 +130,25 @@ func (t *standardTokenHandler) AuthorizeRequest(req *http.Request, params map[st
return nil
}
type sessionTokenHandler struct {
sessionID string
type usernameTokenHandler struct {
username string
}
func NewSessionTokenHandler(sessionID string) Handler {
return &sessionTokenHandler{
sessionID: sessionID,
// NewUsernameTokenHandler returns a handler which will generate
// a token according the user's privileges
func NewUsernameTokenHandler(username string) Handler {
return &usernameTokenHandler{
username: username,
}
}
func (s *sessionTokenHandler) Schema() string {
// Schema implements the corresponding method in interface AuthHandler
func (u *usernameTokenHandler) Schema() string {
return "bearer"
}
func (s *sessionTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
session, err := beego.GlobalSessions.GetSessionStore(s.sessionID)
if err != nil {
return err
}
username, ok := session.Get("username").(string)
if !ok {
return errors.New("username in session can not be converted to string")
}
if len(username) == 0 {
userID, ok := session.Get("userId").(int)
if !ok {
return errors.New("userId in session can not be converted to int")
}
u := models.User{
UserID: userID,
}
user, err := dao.GetUser(u)
if err != nil {
return err
}
if user == nil {
return fmt.Errorf("user with id %d does not exist", userID)
}
username = user.Username
}
// AuthorizeRequest implements the corresponding method in interface AuthHandler
func (u *usernameTokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
service := params["service"]
scopes := []string{}
@ -187,14 +157,14 @@ func (s *sessionTokenHandler) AuthorizeRequest(req *http.Request, params map[str
scopes = strings.Split(scope, " ")
}
token, err := utils.GenTokenForUI(username, service, scopes)
token, err := token_util.GenTokenForUI(u.username, service, scopes)
if err != nil {
return err
}
req.Header.Add(http.CanonicalHeaderKey("Authorization"), fmt.Sprintf("Bearer %s", token))
log.Debugf("sessionTokenHandler generated token successfully | %s %s", req.Method, req.URL)
log.Debugf("usernameTokenHandler generated token successfully | %s %s", req.Method, req.URL)
return nil
}

38
utils/registry/error.go Normal file
View File

@ -0,0 +1,38 @@
/*
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"fmt"
)
// Error : if response's status code is not 200 or does not meet requirement,
// an Error instance will be returned
type Error struct {
StatusCode int
Message string
}
// Error ...
func (e Error) Error() string {
return fmt.Sprintf("%d %s", e.StatusCode, e.Message)
}
// ParseError parses err, if err is type Error, convert it to Error
func ParseError(err error) (Error, bool) {
e, ok := err.(Error)
return e, ok
}

View File

@ -22,9 +22,9 @@ import (
"github.com/vmware/harbor/utils/registry/auth"
)
// NewHTTPClientAuthHandlersEmbeded return a http.Client which will authorize the request
// and send it again when encounters a 401 error
func NewClientStandardAuthHandlersEmbeded(credential *auth.Credential) *http.Client {
// NewClientStandardAuthHandlerEmbeded return a http.Client which will authorize the request
// according to the credential provided and send it again when encounters a 401 error
func NewClientStandardAuthHandlerEmbeded(credential *auth.Credential) *http.Client {
handlers := []auth.Handler{}
tokenHandler := auth.NewStandardTokenHandler(credential)
@ -38,10 +38,12 @@ func NewClientStandardAuthHandlersEmbeded(credential *auth.Credential) *http.Cli
}
}
func NewClientSessionAuthHandlersEmbeded(sessionID string) *http.Client {
// NewClientUsernameAuthHandlerEmbeded return a http.Client which will authorize the request
// according to the user's privileges and send it again when encounters a 401 error
func NewClientUsernameAuthHandlerEmbeded(username string) *http.Client {
handlers := []auth.Handler{}
tokenHandler := auth.NewSessionTokenHandler(sessionID)
tokenHandler := auth.NewUsernameTokenHandler(username)
handlers = append(handlers, tokenHandler)

View File

@ -16,6 +16,7 @@
package registry
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
@ -51,6 +52,47 @@ func New(endpoint string, client *http.Client) (*Registry, error) {
}, nil
}
// ListTag ...
func (r *Registry) ListTag(name string) ([]string, error) {
tags := []string{}
req, err := http.NewRequest("GET", r.ub.buildTagListURL(name), nil)
if err != nil {
return tags, err
}
resp, err := r.client.Do(req)
if err != nil {
return tags, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return tags, err
}
if resp.StatusCode == http.StatusOK {
tagsResp := struct {
Tags []string `json:"tags"`
}{}
if err := json.Unmarshal(b, &tagsResp); err != nil {
return tags, err
}
tags = tagsResp.Tags
return tags, nil
}
return tags, Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
// ManifestExist ...
func (r *Registry) ManifestExist(name, reference string) (digest string, exist bool, err error) {
req, err := http.NewRequest("HEAD", r.ub.buildManifestURL(name, reference), nil)
@ -58,7 +100,8 @@ func (r *Registry) ManifestExist(name, reference string) (digest string, exist b
return
}
// Schema 2 manifest
// request Schema 2 manifest, if the registry does not support it,
// Schema 1 manifest will be returned
req.Header.Set(http.CanonicalHeaderKey("Accept"), schema2.MediaTypeManifest)
resp, err := r.client.Do(req)
@ -78,12 +121,15 @@ func (r *Registry) ManifestExist(name, reference string) (digest string, exist b
defer resp.Body.Close()
message, err := ioutil.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
err = fmt.Errorf("%s %s : %s %s", req.Method, req.URL, resp.Status, string(message))
err = Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
return
}
@ -104,20 +150,23 @@ func (r *Registry) PullManifest(name, reference string) (digest, mediaType strin
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
mediaType = resp.Header.Get(http.CanonicalHeaderKey("Content-Type"))
payload, err = ioutil.ReadAll(resp.Body)
return
}
message, err := ioutil.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
err = fmt.Errorf("%s %s : %s %s", req.Method, req.URL, resp.Status, string(message))
if resp.StatusCode == http.StatusOK {
digest = resp.Header.Get(http.CanonicalHeaderKey("Docker-Content-Digest"))
mediaType = resp.Header.Get(http.CanonicalHeaderKey("Content-Type"))
payload = b
return
}
err = Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
return
}
@ -139,21 +188,30 @@ func (r *Registry) DeleteManifest(name, digest string) error {
defer resp.Body.Close()
message, err := ioutil.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("%s %s : %s %s", req.Method, req.URL, resp.Status, string(message))
return Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
// DeleteTag ...
func (r *Registry) DeleteTag(name, tag string) error {
digest, _, err := r.ManifestExist(name, tag)
digest, exist, err := r.ManifestExist(name, tag)
if err != nil {
return err
}
if !exist {
return Error{
StatusCode: http.StatusNotFound,
}
}
return r.DeleteManifest(name, digest)
}
@ -175,12 +233,19 @@ func (r *Registry) DeleteBlob(name, digest string) error {
defer resp.Body.Close()
message, err := ioutil.ReadAll(resp.Body)
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("%s %s : %s %s", req.Method, req.URL, resp.Status, string(message))
return Error{
StatusCode: resp.StatusCode,
Message: string(b),
}
}
func (u *uRLBuilder) buildTagListURL(name string) string {
return fmt.Sprintf("%s/v2/%s/tags/list", u.root.String(), name)
}
func (u *uRLBuilder) buildManifestURL(name, reference string) string {

View File

@ -13,7 +13,7 @@
limitations under the License.
*/
package utils
package registry
import (
"errors"
@ -23,6 +23,7 @@ import (
"os"
"regexp"
token_util "github.com/vmware/harbor/service/token"
"github.com/vmware/harbor/utils/log"
)
@ -43,9 +44,9 @@ func BuildRegistryURL(segments ...string) string {
return url
}
// RegistryAPIGet triggers GET request to the URL which is the endpoint of registry and returns the response body.
// APIGet triggers GET request to the URL which is the endpoint of registry and returns the response body.
// It will attach a valid jwt token to the request if registry requires.
func RegistryAPIGet(url, username string) ([]byte, error) {
func APIGet(url, username string) ([]byte, error) {
log.Debugf("Registry API url: %s", url)
response, err := http.Get(url)
@ -71,7 +72,7 @@ func RegistryAPIGet(url, username string) ([]byte, error) {
service = res[1]
scopes = append(scopes, res[2])
}
token, err := GenTokenForUI(username, service, scopes)
token, err := token_util.GenTokenForUI(username, service, scopes)
if err != nil {
return nil, err
}