When a model's owner (user/org) isn't in the token's fine-grained scopes AND the model itself isn't directly scoped, the Permission can end up blank, which is a proto violation. So, move those cases to unbounded resources.
702 lines
20 KiB
Go
702 lines
20 KiB
Go
//go:generate generate_permissions permissions.yaml permissions.go huggingface
|
|
|
|
package huggingface
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/jedib0t/go-pretty/v6/table"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
)
|
|
|
|
const (
|
|
FINEGRAINED = "fineGrained"
|
|
WRITE = "write"
|
|
READ = "read"
|
|
)
|
|
|
|
var _ analyzers.Analyzer = (*Analyzer)(nil)
|
|
|
|
type Analyzer struct {
|
|
Cfg *config.Config
|
|
}
|
|
|
|
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeHuggingFace }
|
|
|
|
func (a Analyzer) Analyze(ctx context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
|
key, ok := credInfo["key"]
|
|
if !ok || key == "" {
|
|
err := fmt.Errorf("key not found in credentialInfo")
|
|
ctx.Logger().Error(err, "huggingface credentials missing key")
|
|
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationValidateCredentials, analyzers.ServiceConfig, "", err)
|
|
}
|
|
|
|
info, err := AnalyzePermissions(ctx, a.Cfg, key)
|
|
if err != nil {
|
|
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationAnalyzePermissions, analyzers.ServiceAPI, "", err)
|
|
}
|
|
return secretInfoToAnalyzerResult(info), nil
|
|
}
|
|
|
|
func bakeUnboundedResources(tokenJSON HFTokenJSON) []analyzers.Resource {
|
|
unboundedResources := make([]analyzers.Resource, len(tokenJSON.Orgs))
|
|
for idx, org := range tokenJSON.Orgs {
|
|
unboundedResources[idx] = analyzers.Resource{
|
|
Name: org.Name,
|
|
FullyQualifiedName: "huggingface.com/user/" + tokenJSON.Username + "/organization/" + org.Name,
|
|
Type: "organization",
|
|
Metadata: map[string]interface{}{
|
|
"role": org.Role,
|
|
"is_enterprise": org.IsEnterprise,
|
|
},
|
|
}
|
|
}
|
|
return unboundedResources
|
|
}
|
|
|
|
func bakeUnfineGrainedBindings(allModels []Model, tokenJSON HFTokenJSON) []analyzers.Binding {
|
|
bindings := make([]analyzers.Binding, len(allModels))
|
|
|
|
for idx, model := range allModels {
|
|
// Add Read Privs to All Models
|
|
modelResource := analyzers.Resource{
|
|
Name: model.Name,
|
|
FullyQualifiedName: "huggingface.com/model/" + model.ID,
|
|
Type: "model",
|
|
Metadata: map[string]interface{}{
|
|
"private": model.Private,
|
|
},
|
|
}
|
|
|
|
// means both read & write permission for the model
|
|
accessLevel := string(analyzers.READ)
|
|
if tokenJSON.Auth.AccessToken.Type == WRITE {
|
|
accessLevel = string(analyzers.WRITE)
|
|
}
|
|
bindings[idx] = analyzers.Binding{
|
|
Resource: modelResource,
|
|
Permission: analyzers.Permission{
|
|
Value: string(accessLevel),
|
|
},
|
|
}
|
|
}
|
|
return bindings
|
|
}
|
|
|
|
// bakefineGrainedBindings resolves each model's permission from the token's
|
|
// fine-grained scopes (grouped by org, user, or model).
|
|
func bakefineGrainedBindings(allModels []Model, tokenJSON HFTokenJSON) ([]analyzers.Binding, []analyzers.Resource) {
|
|
var nameToPermissions = make(map[string]analyzers.Permission)
|
|
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
|
|
privs := analyzers.Permission{
|
|
Value: string(analyzers.NONE),
|
|
}
|
|
for _, perm := range permission.Permissions {
|
|
switch perm {
|
|
case "repo.content.read":
|
|
privs.Value = string(analyzers.READ)
|
|
case "repo.write":
|
|
privs.Value = string(analyzers.WRITE)
|
|
}
|
|
}
|
|
switch permission.Entity.Type {
|
|
case "user", "org":
|
|
nameToPermissions[permission.Entity.Name] = privs
|
|
case "model":
|
|
nameToPermissions[modelNameLookup(allModels, permission.Entity.ID)] = privs
|
|
}
|
|
}
|
|
|
|
var bindings []analyzers.Binding
|
|
var unbounded []analyzers.Resource
|
|
for _, model := range allModels {
|
|
modelResource := analyzers.Resource{
|
|
Name: model.Name,
|
|
FullyQualifiedName: "huggingface.com/model/" + model.ID,
|
|
Type: "model",
|
|
Metadata: map[string]interface{}{
|
|
"private": model.Private,
|
|
},
|
|
}
|
|
|
|
var perm analyzers.Permission
|
|
modelUsername := strings.Split(model.Name, "/")[0]
|
|
if permissions, ok := nameToPermissions[modelUsername]; ok {
|
|
perm = permissions
|
|
}
|
|
if permissions, ok := nameToPermissions[model.Name]; ok {
|
|
perm = permissions
|
|
}
|
|
|
|
// Models without an explicit fine-grained scope are visible to the
|
|
// token but have no bound permission; they belong in UnboundedResources.
|
|
if perm.Value == "" {
|
|
unbounded = append(unbounded, modelResource)
|
|
continue
|
|
}
|
|
|
|
bindings = append(bindings, analyzers.Binding{
|
|
Resource: modelResource,
|
|
Permission: perm,
|
|
})
|
|
}
|
|
return bindings, unbounded
|
|
}
|
|
|
|
func bakeOrganizationBindings(tokenJSON HFTokenJSON) []analyzers.Binding {
|
|
// check if there are any org permissions
|
|
// if so, save them as a map. Only need to do this once
|
|
// even if multiple orgs b/c as of 6/6/24, users can only define one set of scopes
|
|
// for all orgs referenced on an access token
|
|
orgPermissions := map[string]struct{}{}
|
|
var orgResource *analyzers.Resource = nil
|
|
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
|
|
if permission.Entity.Type == "org" {
|
|
orgResource = &analyzers.Resource{
|
|
Name: permission.Entity.Name,
|
|
FullyQualifiedName: "hugggingface.com/organization/" + permission.Entity.ID,
|
|
Type: "organization",
|
|
}
|
|
for _, perm := range permission.Permissions {
|
|
orgPermissions[perm] = struct{}{}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
bindings := make([]analyzers.Binding, 0)
|
|
// check if there are any org permissions
|
|
if orgResource == nil {
|
|
return bindings
|
|
}
|
|
|
|
for _, permission := range org_scopes_order {
|
|
for key, value := range org_scopes[permission] {
|
|
if _, ok := orgPermissions[key]; ok {
|
|
bindings = append(bindings, analyzers.Binding{
|
|
Resource: *orgResource,
|
|
Permission: analyzers.Permission{
|
|
Value: value,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return bindings
|
|
}
|
|
|
|
func bakeUserBindings(tokenJSON HFTokenJSON) []analyzers.Binding {
|
|
bindings := make([]analyzers.Binding, 0)
|
|
// build a map of all user permissions
|
|
users := map[string]struct{}{}
|
|
userPermissions := map[string]struct{}{}
|
|
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
|
|
if permission.Entity.Type == "user" {
|
|
users[permission.Entity.Name] = struct{}{}
|
|
for _, perm := range permission.Permissions {
|
|
userPermissions[perm] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// global permissions only apply to user tokens as of 6/6/24
|
|
// but there would be a naming collision in the scopes document
|
|
// so we prepend "global." to the key and then add to the map
|
|
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Global {
|
|
userPermissions["global."+permission] = struct{}{}
|
|
}
|
|
|
|
// check if there are any user permissions
|
|
if len(userPermissions) == 0 {
|
|
return bindings
|
|
}
|
|
|
|
userResource := analyzers.Resource{
|
|
Name: tokenJSON.Name,
|
|
FullyQualifiedName: "huggingface.com/user/" + tokenJSON.Username,
|
|
Type: "user",
|
|
}
|
|
for _, permission := range user_scopes_order {
|
|
for key, value := range user_scopes[permission] {
|
|
if _, ok := userPermissions[key]; ok {
|
|
bindings = append(bindings, analyzers.Binding{
|
|
Resource: userResource,
|
|
Permission: analyzers.Permission{
|
|
Value: value,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return bindings
|
|
}
|
|
|
|
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
|
if info == nil {
|
|
return nil
|
|
}
|
|
|
|
result := analyzers.AnalyzerResult{
|
|
AnalyzerType: analyzers.AnalyzerTypeHuggingFace,
|
|
Metadata: map[string]interface{}{
|
|
"username": info.Token.Username,
|
|
"name": info.Token.Name,
|
|
"token_name": info.Token.Auth.AccessToken.Name,
|
|
"token_type": info.Token.Auth.AccessToken.Type,
|
|
},
|
|
}
|
|
|
|
if len(info.Token.Orgs) > 0 {
|
|
result.UnboundedResources = bakeUnboundedResources(info.Token)
|
|
}
|
|
|
|
result.Bindings = make([]analyzers.Binding, 0)
|
|
|
|
if info.Token.Auth.AccessToken.Type == FINEGRAINED {
|
|
fgBindings, fgUnbounded := bakefineGrainedBindings(info.Models, info.Token)
|
|
result.Bindings = append(result.Bindings, fgBindings...)
|
|
result.UnboundedResources = append(result.UnboundedResources, fgUnbounded...)
|
|
result.Bindings = append(result.Bindings, bakeOrganizationBindings(info.Token)...)
|
|
result.Bindings = append(result.Bindings, bakeUserBindings(info.Token)...)
|
|
} else {
|
|
result.Bindings = append(result.Bindings, bakeUnfineGrainedBindings(info.Models, info.Token)...)
|
|
}
|
|
|
|
return &result
|
|
}
|
|
|
|
// HFTokenJSON is the struct for the HF /whoami-v2 API JSON response
|
|
type HFTokenJSON struct {
|
|
Username string `json:"name"`
|
|
Name string `json:"fullname"`
|
|
Orgs []struct {
|
|
Name string `json:"name"`
|
|
Role string `json:"roleInOrg"`
|
|
IsEnterprise bool `json:"isEnterprise"`
|
|
} `json:"orgs"`
|
|
Auth struct {
|
|
AccessToken struct {
|
|
Name string `json:"displayName"`
|
|
Type string `json:"role"`
|
|
CreatedAt string `json:"createdAt"`
|
|
FineGrained struct {
|
|
Global []string `json:"global"`
|
|
Scoped []struct {
|
|
Entity struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
ID string `json:"_id"`
|
|
} `json:"entity"`
|
|
Permissions []string `json:"permissions"`
|
|
} `json:"scoped"`
|
|
} `json:"fineGrained"`
|
|
}
|
|
} `json:"auth"`
|
|
}
|
|
|
|
type Permissions struct {
|
|
Read bool
|
|
Write bool
|
|
}
|
|
|
|
type Model struct {
|
|
Name string `json:"id"`
|
|
ID string `json:"_id"`
|
|
Private bool `json:"private"`
|
|
Permissions Permissions
|
|
}
|
|
|
|
// getModelsByAuthor calls the HF API /models endpoint with the author query param
|
|
// returns a list of models and an error
|
|
func getModelsByAuthor(ctx context.Context, cfg *config.Config, key string, author string) ([]Model, error) {
|
|
var modelsJSON []Model
|
|
|
|
// create a new request
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://huggingface.co/api/models", nil)
|
|
if err != nil {
|
|
ctx.Logger().Error(err, "failed to create huggingface models request", "author", author)
|
|
return modelsJSON, err
|
|
}
|
|
|
|
// Add bearer token
|
|
req.Header.Add("Authorization", "Bearer "+key)
|
|
|
|
// Add author param
|
|
q := req.URL.Query()
|
|
q.Add("author", author)
|
|
req.URL.RawQuery = q.Encode()
|
|
|
|
// send the request
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
ctx.Logger().Error(err, "failed to send huggingface models request", "author", author)
|
|
return modelsJSON, err
|
|
}
|
|
|
|
// defer the response body closing
|
|
defer func() {
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
}()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
err := fmt.Errorf("unexpected status code: %d while fetching models for author %s", resp.StatusCode, author)
|
|
ctx.Logger().Error(err, "unexpected status code while fetching huggingface models", "author", author, "status_code", resp.StatusCode)
|
|
return modelsJSON, err
|
|
}
|
|
|
|
// read response
|
|
if err := json.NewDecoder(resp.Body).Decode(&modelsJSON); err != nil {
|
|
ctx.Logger().Error(err, "failed to decode huggingface models response", "author", author)
|
|
return modelsJSON, err
|
|
}
|
|
return modelsJSON, nil
|
|
}
|
|
|
|
// getTokenInfo calls the HF API /whoami-v2 endpoint to get the token info
|
|
// returns the token info, a boolean indicating token validity, and an error
|
|
func getTokenInfo(ctx context.Context, cfg *config.Config, key string) (HFTokenJSON, bool, error) {
|
|
var tokenJSON HFTokenJSON
|
|
|
|
// create a new request
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
req, err := http.NewRequestWithContext(ctx, "GET", "https://huggingface.co/api/whoami-v2", nil)
|
|
if err != nil {
|
|
ctx.Logger().Error(err, "failed to create huggingface whoami request")
|
|
return tokenJSON, false, err
|
|
}
|
|
|
|
// Add bearer token
|
|
req.Header.Add("Authorization", "Bearer "+key)
|
|
|
|
// send the request
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
ctx.Logger().Error(err, "failed to send huggingface whoami request")
|
|
return tokenJSON, false, err
|
|
}
|
|
|
|
// defer the response body closing
|
|
defer func() {
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
_ = resp.Body.Close()
|
|
}()
|
|
|
|
// check if the response is 200
|
|
if resp.StatusCode != http.StatusOK {
|
|
ctx.Logger().V(2).Info("huggingface token invalid while fetching whoami", "status_code", resp.StatusCode)
|
|
return tokenJSON, false, nil
|
|
}
|
|
|
|
// read response
|
|
if err := json.NewDecoder(resp.Body).Decode(&tokenJSON); err != nil {
|
|
ctx.Logger().Error(err, "failed to decode huggingface whoami response")
|
|
return tokenJSON, true, err
|
|
}
|
|
return tokenJSON, true, nil
|
|
}
|
|
|
|
type SecretInfo struct {
|
|
Token HFTokenJSON
|
|
Models []Model
|
|
}
|
|
|
|
func AnalyzePermissions(ctx context.Context, cfg *config.Config, key string) (*SecretInfo, error) {
|
|
// get token info
|
|
token, success, err := getTokenInfo(ctx, cfg, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !success {
|
|
err := fmt.Errorf("invalid HuggingFace Access Token")
|
|
ctx.Logger().V(2).Info("huggingface access token validation failed")
|
|
return nil, err
|
|
}
|
|
|
|
// get all models by username
|
|
var allModels []Model
|
|
userModels, err := getModelsByAuthor(ctx, cfg, key, token.Username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allModels = append(allModels, userModels...)
|
|
|
|
// get all models from all orgs
|
|
for _, org := range token.Orgs {
|
|
orgModels, err := getModelsByAuthor(ctx, cfg, key, org.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
allModels = append(allModels, orgModels...)
|
|
}
|
|
|
|
return &SecretInfo{
|
|
Token: token,
|
|
Models: allModels,
|
|
}, nil
|
|
}
|
|
|
|
// AnalyzeAndPrintPermissions prints the permissions of a HuggingFace API key
|
|
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
|
info, err := AnalyzePermissions(context.Background(), cfg, key)
|
|
if err != nil {
|
|
color.Red("[x] Error: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
color.Green("[!] Valid HuggingFace Access Token\n\n")
|
|
|
|
// print user info
|
|
color.Yellow("[i] Username: " + info.Token.Username)
|
|
color.Yellow("[i] Name: " + info.Token.Name)
|
|
color.Yellow("[i] Token Name: " + info.Token.Auth.AccessToken.Name)
|
|
color.Yellow("[i] Token Type: " + info.Token.Auth.AccessToken.Type)
|
|
|
|
// print org info
|
|
printOrgs(info.Token)
|
|
|
|
// print accessible models
|
|
printAccessibleModels(info.Models, info.Token)
|
|
|
|
if info.Token.Auth.AccessToken.Type == FINEGRAINED {
|
|
// print org permissions
|
|
printOrgPermissions(info.Token)
|
|
|
|
// print user permissions
|
|
printUserPermissions(info.Token)
|
|
}
|
|
}
|
|
|
|
// printUserPermissions prints the user permissions
|
|
// only applies to fine-grained tokens
|
|
func printUserPermissions(tokenJSON HFTokenJSON) {
|
|
color.Green("\n[i] User Permissions:")
|
|
|
|
// build a map of all user permissions
|
|
userPermissions := map[string]struct{}{}
|
|
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
|
|
if permission.Entity.Type == "user" {
|
|
for _, perm := range permission.Permissions {
|
|
userPermissions[perm] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// global permissions only apply to user tokens as of 6/6/24
|
|
// but there would be a naming collision in the scopes document
|
|
// so we prepend "global." to the key and then add to the map
|
|
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Global {
|
|
userPermissions["global."+permission] = struct{}{}
|
|
}
|
|
|
|
// check if there are any user permissions
|
|
if len(userPermissions) == 0 {
|
|
color.Red("\tNo user permissions scoped.")
|
|
return
|
|
}
|
|
|
|
// print the user permissions
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Category", "Permission", "In-Scope"})
|
|
|
|
for _, permission := range user_scopes_order {
|
|
t.AppendRow([]interface{}{permission, "---", "---"})
|
|
for key, value := range user_scopes[permission] {
|
|
if _, ok := userPermissions[key]; ok {
|
|
t.AppendRow([]interface{}{"", color.GreenString(value), color.GreenString("True")})
|
|
} else {
|
|
t.AppendRow([]interface{}{"", value, "False"})
|
|
}
|
|
}
|
|
}
|
|
t.Render()
|
|
}
|
|
|
|
// printOrgPermissions prints the organization permissions
|
|
// only applies to fine-grained tokens
|
|
func printOrgPermissions(tokenJSON HFTokenJSON) {
|
|
color.Green("\n[i] Organization Permissions:")
|
|
|
|
// check if there are any org permissions
|
|
// if so, save them as a map. Only need to do this once
|
|
// even if multiple orgs b/c as of 6/6/24, users can only define one set of scopes
|
|
// for all orgs referenced on an access token
|
|
orgScoped := false
|
|
orgPermissions := map[string]struct{}{}
|
|
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
|
|
if permission.Entity.Type == "org" {
|
|
orgScoped = true
|
|
for _, perm := range permission.Permissions {
|
|
orgPermissions[perm] = struct{}{}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
// check if there are any org permissions
|
|
if !orgScoped {
|
|
color.Red("\tNo organization permissions scoped.")
|
|
return
|
|
}
|
|
|
|
// print the org permissions
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Category", "Permission", "In-Scope"})
|
|
|
|
for _, permission := range org_scopes_order {
|
|
t.AppendRow([]interface{}{permission, "---", "---"})
|
|
for key, value := range org_scopes[permission] {
|
|
if _, ok := orgPermissions[key]; ok {
|
|
t.AppendRow([]interface{}{"", color.GreenString(value), color.GreenString("True")})
|
|
} else {
|
|
t.AppendRow([]interface{}{"", value, "False"})
|
|
}
|
|
}
|
|
}
|
|
t.Render()
|
|
}
|
|
|
|
// printOrgs prints the organizations the user is a member of
|
|
func printOrgs(tokenJSON HFTokenJSON) {
|
|
color.Green("\n[i] Organizations:")
|
|
|
|
if len(tokenJSON.Orgs) == 0 {
|
|
color.Yellow("\tNo organizations found.")
|
|
return
|
|
}
|
|
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Name", "Role", "Is Enterprise"})
|
|
for _, org := range tokenJSON.Orgs {
|
|
enterprise := ""
|
|
role := ""
|
|
if org.IsEnterprise {
|
|
enterprise = color.New(color.FgGreen).Sprint("True")
|
|
} else {
|
|
enterprise = "False"
|
|
}
|
|
if org.Role == "admin" {
|
|
role = color.New(color.FgGreen).Sprint("Admin")
|
|
} else {
|
|
role = org.Role
|
|
}
|
|
t.AppendRow([]interface{}{color.GreenString(org.Name), role, enterprise})
|
|
}
|
|
t.Render()
|
|
}
|
|
|
|
// modelNameLookup is a helper function to lookup model name by _id
|
|
func modelNameLookup(models []Model, id string) string {
|
|
for _, model := range models {
|
|
if model.ID == id {
|
|
return model.Name
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// printAccessibleModels adds permissions as needed to each model
|
|
//
|
|
// and then calls the printModelsTable function
|
|
func printAccessibleModels(allModels []Model, tokenJSON HFTokenJSON) {
|
|
color.Green("\n[i] Accessible Models:")
|
|
|
|
if tokenJSON.Auth.AccessToken.Type != FINEGRAINED {
|
|
// Add Read Privs to All Models
|
|
for idx := range allModels {
|
|
allModels[idx].Permissions.Read = true
|
|
}
|
|
// Add Write Privs to All Models if Write Access
|
|
if tokenJSON.Auth.AccessToken.Type == WRITE {
|
|
for idx := range allModels {
|
|
allModels[idx].Permissions.Write = true
|
|
}
|
|
}
|
|
// Print Models Table
|
|
printModelsTable(allModels)
|
|
return
|
|
}
|
|
|
|
// finegrained scopes are grouped by org, user or model.
|
|
// this section will extract the relevant permissions for each entity and store them in a map
|
|
var nameToPermissions = make(map[string]Permissions)
|
|
for _, permission := range tokenJSON.Auth.AccessToken.FineGrained.Scoped {
|
|
read := false
|
|
write := false
|
|
for _, perm := range permission.Permissions {
|
|
switch perm {
|
|
case "repo.content.read":
|
|
read = true
|
|
case "repo.write":
|
|
write = true
|
|
}
|
|
}
|
|
switch permission.Entity.Type {
|
|
case "user", "org":
|
|
nameToPermissions[permission.Entity.Name] = Permissions{Read: read, Write: write}
|
|
case "model":
|
|
nameToPermissions[modelNameLookup(allModels, permission.Entity.ID)] = Permissions{Read: read, Write: write}
|
|
}
|
|
}
|
|
|
|
// apply permissions to all models
|
|
for idx := range allModels {
|
|
// get username/orgname for each model and apply those permissions
|
|
modelUsername := strings.Split(allModels[idx].Name, "/")[0]
|
|
if permissions, ok := nameToPermissions[modelUsername]; ok {
|
|
allModels[idx].Permissions = permissions
|
|
}
|
|
// override model permissions with repo-specific permissions
|
|
if permissions, ok := nameToPermissions[allModels[idx].Name]; ok {
|
|
allModels[idx].Permissions = permissions
|
|
}
|
|
}
|
|
|
|
// Print Models Table
|
|
printModelsTable(allModels)
|
|
}
|
|
|
|
// printModelsTable prints the models table
|
|
func printModelsTable(models []Model) {
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Model", "Private", "Read", "Write"})
|
|
for _, model := range models {
|
|
var name, read, write, private string
|
|
if model.Permissions.Read {
|
|
read = color.New(color.FgGreen).Sprint("True")
|
|
} else {
|
|
read = "False"
|
|
}
|
|
if model.Permissions.Write {
|
|
write = color.New(color.FgGreen).Sprint("True")
|
|
} else {
|
|
write = "False"
|
|
}
|
|
if model.Private {
|
|
private = color.New(color.FgGreen).Sprint("True")
|
|
name = color.New(color.FgGreen).Sprint(model.Name)
|
|
} else {
|
|
private = "False"
|
|
name = model.Name
|
|
}
|
|
t.AppendRow([]interface{}{name, private, read, write})
|
|
}
|
|
t.Render()
|
|
}
|